// SPDX-License-Identifier: AGPL-3.0-only use std::sync::Arc; use std::time::Duration; use std::sync::LazyLock; use tokio::sync::RwLock; use reqwest::{StatusCode, Client}; use urlencoding::encode as urlencode; use super::{CacheFuture, CacheGetter, Cache, AsyncCache}; use crate::deserialize::{User, Track, TrackStub}; use crate::CONFIG; pub type UserInfo = Arc<(User, Track, TrackStub)>; type UserFuture = CacheFuture>; type UserGetter = CacheGetter>; type UserCache = Cache>; #[derive(Debug)] enum Allowlist { Exclusive{cache: UserCache}, Open{default_cache: UserCache, allowlist_cache: UserCache} } fn user_getter(username: &String) -> UserFuture { use crate::deserialize::{GetUserInfo, GetRecentTracks, GetTrackInfo}; let username = urlencoding::encode(username.as_ref()).to_string(); Box::pin(async move { let userreq = HTTP.get(format!("https://ws.audioscrobbler.com/2.0/?method=user.getInfo&format=json&user={username}&api_key={}", CONFIG.lastfm_api_key)) .send().await .map_err(|e| {log::error!("Failed to get info for user `{username}`: {e}"); (StatusCode::SERVICE_UNAVAILABLE, "Couldn't connect to last.fm!")})?; if userreq.status() == StatusCode::NOT_FOUND { return Err((StatusCode::NOT_FOUND, "User does not exist!")); } let userstr = userreq.text().await.unwrap(); log::trace!("Got user.getUserInfo JSON for `{username}`: {userstr}"); let userinfo = serde_json::from_str::(&userstr) .map_err(|e| {log::error!("Couldn't parse user.getInfo for `{username}`: {e}"); (StatusCode::INTERNAL_SERVER_ERROR, "Couldn't parse user.getInfo!")})?.user; log::trace!("Parsed into: {userinfo:?}"); let tracksreq = HTTP.get(format!("https://ws.audioscrobbler.com/2.0/?method=user.getRecentTracks&format=json&extended=1&limit=1&user={username}&api_key={}", CONFIG.lastfm_api_key)) .send().await .map_err(|e| {log::error!("Failed to get tracks for user `{username}`: {e}"); (StatusCode::SERVICE_UNAVAILABLE, "Couldn't connect to last.fm!")})?; if tracksreq.status() == StatusCode::NOT_FOUND { return Err((StatusCode::NOT_FOUND, "User does not exist!")); } if tracksreq.status() == StatusCode::FORBIDDEN { return Err((StatusCode::FORBIDDEN, "You need to unprivate your song history!")); } let tracksstr = tracksreq.text().await.unwrap(); log::trace!("Got user.getRecentTracks JSON for `{username}`: {tracksstr}"); let trackstub = serde_json::from_str::(&tracksstr) .map_err(|e| {log::error!("Couldn't parse user.getRecentTracks for `{username}`: {e}"); (StatusCode::INTERNAL_SERVER_ERROR, "Couldn't parse user.getRecentTracks!")})? .recenttracks.track.into_iter().next().ok_or((StatusCode::UNPROCESSABLE_ENTITY, "You need to listen to some songs first!"))?; log::trace!("Parsed into: {trackstub:?}"); let trackreq = HTTP.get(format!("https://ws.audioscrobbler.com/2.0/?method=track.getInfo&format=json&username={username}&api_key={}&track={}&artist={}", CONFIG.lastfm_api_key, urlencode(&trackstub.name), urlencode(&trackstub.artist.name))) .send().await .map_err(|e| {log::error!("Failed to get tracks for user `{username}`: {e}"); (StatusCode::SERVICE_UNAVAILABLE, "Couldn't connect to last.fm!")})?; if trackreq.status() == StatusCode::NOT_FOUND { return Err((StatusCode::NOT_FOUND, "Track does not exist!")); } let trackstr = trackreq.text().await.unwrap(); log::trace!("Got track.getInfo JSON for `{username}`: {trackstr}"); let trackinfo = serde_json::from_str::(&trackstr) .map_err(|e| {log::error!("Couldn't parse track.getInfo for `{}` by `{}` on behalf of {username}: {e}", trackstub.name, trackstub.artist.name); (StatusCode::INTERNAL_SERVER_ERROR, "Couldn't parse track.getInfo!")})?.track; log::trace!("Parsed into: {trackinfo:?}"); Ok(Arc::new((userinfo, trackinfo, trackstub))) }) } static HTTP: LazyLock = crate::http::lazy(); static ALLOWLIST: LazyLock = LazyLock::new(|| { let default_cache = Arc::new(RwLock::new(AsyncCache::new(CONFIG.default_refresh, user_getter as UserGetter))); let allowlist_cache = Arc::new(RwLock::new(AsyncCache::new(CONFIG.allowlist_refresh, user_getter as UserGetter))); match CONFIG.allowlist_mode.as_str() { "open" => { Allowlist::Open{default_cache, allowlist_cache} }, "exclusive" => { if CONFIG.allowlist.is_empty() { panic!("Exclusive mode set with empty allowlist, cannot serve any requests!"); } Allowlist::Exclusive{cache: allowlist_cache} }, m => { panic!("Bad allowlist mode: `{m}`"); } } }); pub async fn get_userinfo(user: &String) -> (Result, (StatusCode, &'static str)>, Duration) { match LazyLock::force(&ALLOWLIST) { Allowlist::Open{default_cache, allowlist_cache} => { if CONFIG.allowlist.contains(user) { (allowlist_cache.write().await.get_owned(user).await, CONFIG.allowlist_refresh) } else { (default_cache.write().await.get_owned(user).await, CONFIG.default_refresh) } }, Allowlist::Exclusive{cache} => { if CONFIG.allowlist.contains(user) { (cache.write().await.get_owned(user).await, CONFIG.allowlist_refresh) } else { (Err((StatusCode::FORBIDDEN, "User not in allowlist!")), CONFIG.default_refresh) } } } } pub fn touch() { LazyLock::force(&ALLOWLIST); }