queryArtistOverview / artist stats (monthly listeners)
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"
}
]
}
},
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.
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(¶ms);
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:?}\"");
}
}