[GH-ISSUE #443] queryArtistOverview / artist stats (monthly listeners) #145

Open
opened 2026-02-27 20:23:23 +03:00 by kerem · 3 comments
Owner

Originally created by @brandonros on GitHub (Nov 2, 2023).
Original GitHub issue: https://github.com/ramsayleung/rspotify/issues/443

Is your feature request related to a problem? Please describe.

At the moment the API does not provide a way to achieve the same level of functionality/data the official Spotify app provides.

Describe the solution you'd like

Add some sort of wrapper around this API endpoint: https://api-partner.spotify.com/pathfinder/v1/query?operationName=queryArtistOverview&variables={"uri":"spotify:artist:ARTIST_ID","locale":"","includePrerelease":true}&extensions={"persistedQuery":{"version":1,"sha256Hash":"79a4a9d7c3a3781d801e62b62ef11c7ee56fce2626772eb26cd20c69f84b3f49"}}

Describe alternatives you've considered

None.

Additional context

https://github.com/search?q=spotify+queryArtistOverview&type=code

It returns a ton of data, such as:

"stats": {
                "followers": 606420,
                "monthlyListeners": 5744472,
                "worldRank": 0,
                "topCities": {
                    "items": [
                        {
                            "numberOfListeners": 142247,
                            "city": "Quezon City",
                            "country": "PH",
                            "region": "00"
                        },
                        {
                            "numberOfListeners": 97895,
                            "city": "Sydney",
                            "country": "AU",
                            "region": "NSW"
                        },
                        {
                            "numberOfListeners": 97663,
                            "city": "Jakarta",
                            "country": "ID",
                            "region": "JK"
                        },
                        {
                            "numberOfListeners": 87974,
                            "city": "São Paulo",
                            "country": "BR",
                            "region": "SP"
                        },
                        {
                            "numberOfListeners": 81795,
                            "city": "Melbourne",
                            "country": "AU",
                            "region": "VIC"
                        }
                    ]
                }
            },
Originally created by @brandonros on GitHub (Nov 2, 2023). Original GitHub issue: https://github.com/ramsayleung/rspotify/issues/443 **Is your feature request related to a problem? Please describe.** At the moment the API does not provide a way to achieve the same level of functionality/data the official Spotify app provides. **Describe the solution you'd like** Add some sort of wrapper around this API endpoint: `https://api-partner.spotify.com/pathfinder/v1/query?operationName=queryArtistOverview&variables={"uri":"spotify:artist:ARTIST_ID","locale":"","includePrerelease":true}&extensions={"persistedQuery":{"version":1,"sha256Hash":"79a4a9d7c3a3781d801e62b62ef11c7ee56fce2626772eb26cd20c69f84b3f49"}}` **Describe alternatives you've considered** None. **Additional context** https://github.com/search?q=spotify+queryArtistOverview&type=code It returns a ton of data, such as: ``` "stats": { "followers": 606420, "monthlyListeners": 5744472, "worldRank": 0, "topCities": { "items": [ { "numberOfListeners": 142247, "city": "Quezon City", "country": "PH", "region": "00" }, { "numberOfListeners": 97895, "city": "Sydney", "country": "AU", "region": "NSW" }, { "numberOfListeners": 97663, "city": "Jakarta", "country": "ID", "region": "JK" }, { "numberOfListeners": 87974, "city": "São Paulo", "country": "BR", "region": "SP" }, { "numberOfListeners": 81795, "city": "Melbourne", "country": "AU", "region": "VIC" } ] } }, ```
Author
Owner

@brandonros commented on GitHub (Nov 2, 2023):

My guess is partner-api is different than web API which this package aims to cover and we shouldn't/won't pick this feature up.

<!-- gh-comment-id:1790876926 --> @brandonros commented on GitHub (Nov 2, 2023): My guess is `partner-api` is different than `web API` which this package aims to cover and we shouldn't/won't pick this feature up.
Author
Owner

@brandonros commented on GitHub (Nov 2, 2023):

in case anybody wants this for fun to see how hipster their music tastes are in the mean time as a stop gap:

use std::{collections::HashSet, sync::Arc};
use serde::Deserialize;
use futures::stream::TryStreamExt;
use futures_util::lock::Mutex;
use rspotify::{prelude::*, scopes, AuthCodeSpotify, Credentials, OAuth};
use reqwest::{Url, StatusCode};

#[derive(Deserialize)]
pub struct Stats {
    #[serde(rename = "monthlyListeners")]
    pub monthly_listeners: Option<i64>,
}

#[derive(Deserialize)]
pub struct ArtistUnion {
    pub stats: Stats,
}

#[derive(Deserialize)]
struct Data {
    #[serde(rename = "artistUnion")]
    pub artist_union: ArtistUnion,
}

#[derive(Deserialize)]
struct ResponseRoot {
    pub data: Data
}

async fn get_artist_monthly_listeners(artist_id: &str) -> Option<i64> {
    let base_url = "https://api-partner.spotify.com/pathfinder/v1/query";
    let mut url = Url::parse(base_url).unwrap();
    let params = [
        ("operationName", "queryArtistOverview"),
        ("variables", &format!(r#"{{"uri":"{}","locale":"","includePrerelease":true}}"#, artist_id)),
        ("extensions", r#"{"persistedQuery":{"version":1,"sha256Hash":"79a4a9d7c3a3781d801e62b62ef11c7ee56fce2626772eb26cd20c69f84b3f49"}}"#),
    ];
    url.query_pairs_mut().extend_pairs(&params);
    let spotify_access_token = std::env::var("SPOTIFY_ACCESS_TOKEN").unwrap(); // can't use rspotify auth?
    let client = reqwest::Client::new();
    let response = client.get(url)
        .header("authority", "api-partner.spotify.com")
        .header("accept", "application/json")
        .header("accept-language", "en")
        .header("app-platform", "WebPlayer")
        .header("authorization", &format!("Bearer {spotify_access_token}"))
        .header("content-type", "application/json;charset=UTF-8")
        .header("dnt", "1")
        .header("origin", "https://open.spotify.com")
        .header("referer", "https://open.spotify.com/")
        .header("sec-ch-ua", "\"Chromium\";v=\"118\", \"Google Chrome\";v=\"118\", \"Not=A?Brand\";v=\"99\"")
        .header("sec-ch-ua-mobile", "?0")
        .header("sec-ch-ua-platform", "\"macOS\"")
        .header("sec-fetch-dest", "empty")
        .header("sec-fetch-mode", "cors")
        .header("sec-fetch-site", "same-site")
        .header("sec-gpc", "1")
        .header("spotify-app-version", "1.2.24.636.ga951e261")
        .header("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36")
        .send()
        .await.unwrap();
    assert!(response.status() == StatusCode::OK);
    let response_body: ResponseRoot = response.json().await.unwrap();
    let monthly_listeners = response_body.data.artist_union.stats.monthly_listeners;
    monthly_listeners
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
    // init logger
    env_logger::init();
    // build spotify client
    let creds = Credentials::from_env().unwrap();
    let oauth = OAuth::from_env(scopes!("user-library-read")).unwrap();
    let spotify = AuthCodeSpotify::new(creds, oauth);
    let url = spotify.get_authorize_url(false).unwrap();
    spotify.prompt_for_token(&url).await.unwrap();
    // get current user saved tracks
    let stream = spotify.current_user_saved_tracks(None);
    // concurrently build unique hash_set of artists from current_user_saved_tracks
    let hash_set = Arc::new(Mutex::new(HashSet::new()));
    let hash_set_clone = hash_set.clone();
    stream
        .try_for_each_concurrent(10, move |item| {
            // Since we already cloned it before, we can just use it here.
            let local_hash_set = hash_set_clone.clone();
            async move {
                let mut guard = local_hash_set.lock().await;
                for artist in item.track.artists {
                    guard.insert((artist.name, artist.id.unwrap()));
                }
                Ok(())
            }
        })
        .await
        .unwrap();
    // iterate current_user_saved_tracks
    let guard = hash_set.lock().await;
    for (artist_name, artist_id) in &*guard {
        // get stats
        let monthly_listeners = get_artist_monthly_listeners(&format!("{}", artist_id)).await;
        println!("\"{artist_name}\",\"{artist_id}\",\"{monthly_listeners:?}\"");
    }
}
<!-- gh-comment-id:1791243047 --> @brandonros commented on GitHub (Nov 2, 2023): in case anybody wants this for fun to see how hipster their music tastes are in the mean time as a stop gap: ```rust use std::{collections::HashSet, sync::Arc}; use serde::Deserialize; use futures::stream::TryStreamExt; use futures_util::lock::Mutex; use rspotify::{prelude::*, scopes, AuthCodeSpotify, Credentials, OAuth}; use reqwest::{Url, StatusCode}; #[derive(Deserialize)] pub struct Stats { #[serde(rename = "monthlyListeners")] pub monthly_listeners: Option<i64>, } #[derive(Deserialize)] pub struct ArtistUnion { pub stats: Stats, } #[derive(Deserialize)] struct Data { #[serde(rename = "artistUnion")] pub artist_union: ArtistUnion, } #[derive(Deserialize)] struct ResponseRoot { pub data: Data } async fn get_artist_monthly_listeners(artist_id: &str) -> Option<i64> { let base_url = "https://api-partner.spotify.com/pathfinder/v1/query"; let mut url = Url::parse(base_url).unwrap(); let params = [ ("operationName", "queryArtistOverview"), ("variables", &format!(r#"{{"uri":"{}","locale":"","includePrerelease":true}}"#, artist_id)), ("extensions", r#"{"persistedQuery":{"version":1,"sha256Hash":"79a4a9d7c3a3781d801e62b62ef11c7ee56fce2626772eb26cd20c69f84b3f49"}}"#), ]; url.query_pairs_mut().extend_pairs(&params); let spotify_access_token = std::env::var("SPOTIFY_ACCESS_TOKEN").unwrap(); // can't use rspotify auth? let client = reqwest::Client::new(); let response = client.get(url) .header("authority", "api-partner.spotify.com") .header("accept", "application/json") .header("accept-language", "en") .header("app-platform", "WebPlayer") .header("authorization", &format!("Bearer {spotify_access_token}")) .header("content-type", "application/json;charset=UTF-8") .header("dnt", "1") .header("origin", "https://open.spotify.com") .header("referer", "https://open.spotify.com/") .header("sec-ch-ua", "\"Chromium\";v=\"118\", \"Google Chrome\";v=\"118\", \"Not=A?Brand\";v=\"99\"") .header("sec-ch-ua-mobile", "?0") .header("sec-ch-ua-platform", "\"macOS\"") .header("sec-fetch-dest", "empty") .header("sec-fetch-mode", "cors") .header("sec-fetch-site", "same-site") .header("sec-gpc", "1") .header("spotify-app-version", "1.2.24.636.ga951e261") .header("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36") .send() .await.unwrap(); assert!(response.status() == StatusCode::OK); let response_body: ResponseRoot = response.json().await.unwrap(); let monthly_listeners = response_body.data.artist_union.stats.monthly_listeners; monthly_listeners } #[tokio::main(flavor = "current_thread")] async fn main() { // init logger env_logger::init(); // build spotify client let creds = Credentials::from_env().unwrap(); let oauth = OAuth::from_env(scopes!("user-library-read")).unwrap(); let spotify = AuthCodeSpotify::new(creds, oauth); let url = spotify.get_authorize_url(false).unwrap(); spotify.prompt_for_token(&url).await.unwrap(); // get current user saved tracks let stream = spotify.current_user_saved_tracks(None); // concurrently build unique hash_set of artists from current_user_saved_tracks let hash_set = Arc::new(Mutex::new(HashSet::new())); let hash_set_clone = hash_set.clone(); stream .try_for_each_concurrent(10, move |item| { // Since we already cloned it before, we can just use it here. let local_hash_set = hash_set_clone.clone(); async move { let mut guard = local_hash_set.lock().await; for artist in item.track.artists { guard.insert((artist.name, artist.id.unwrap())); } Ok(()) } }) .await .unwrap(); // iterate current_user_saved_tracks let guard = hash_set.lock().await; for (artist_name, artist_id) in &*guard { // get stats let monthly_listeners = get_artist_monthly_listeners(&format!("{}", artist_id)).await; println!("\"{artist_name}\",\"{artist_id}\",\"{monthly_listeners:?}\""); } } ```
Author
Owner

@ofou commented on GitHub (Nov 17, 2024):

This will be very cool since current WebAPI doesn’t plan to add this functionality :(

https://github.com/spotify/web-api/issues/1037

<!-- gh-comment-id:2481523357 --> @ofou commented on GitHub (Nov 17, 2024): This will be very cool since current WebAPI doesn’t plan to add this functionality :( https://github.com/spotify/web-api/issues/1037
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/rspotify#145
No description provided.