diff options
author | alyx <alyx@aleteoryx.me> | 2024-04-01 19:00:57 -0400 |
---|---|---|
committer | alyx <alyx@aleteoryx.me> | 2024-04-01 19:00:57 -0400 |
commit | 22c2e4e2db9ad9d892ed5fb63d92254677f6dafd (patch) | |
tree | c3132945f32d1ca5741848a504b48f09a599bafb | |
parent | dc6875ce18a48d314f0576528ebd7dc9e5e2a1b8 (diff) | |
download | lfm_embed-22c2e4e2db9ad9d892ed5fb63d92254677f6dafd.tar.gz lfm_embed-22c2e4e2db9ad9d892ed5fb63d92254677f6dafd.tar.bz2 lfm_embed-22c2e4e2db9ad9d892ed5fb63d92254677f6dafd.zip |
Reliable user info; Font refactor
Hit a few more endpoints to fix missing images, fallback to default album art just in case.
Refactor the font cache into its own file.
-rw-r--r-- | src/cache.rs | 15 | ||||
-rw-r--r-- | src/config.rs | 53 | ||||
-rw-r--r-- | src/ctx.rs | 12 | ||||
-rw-r--r-- | src/deserialize.rs | 66 | ||||
-rw-r--r-- | src/font.rs | 40 |
5 files changed, 119 insertions, 67 deletions
diff --git a/src/cache.rs b/src/cache.rs index 0e8fd4d..a6f25fa 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,5 +1,14 @@ -use std::{future::Future, time::*, collections::HashMap, hash::Hash}; +use std::future::Future; +use std::time::*; +use std::collections::HashMap; +use std::hash::Hash; +use std::sync::Arc; +use std::pin::Pin; + +use tokio::sync::RwLock; + use reqwest::StatusCode; + #[derive(Debug)] pub struct AsyncCache<K, V, F> { func: F, @@ -66,3 +75,7 @@ where self.get(key).await.cloned() } } + +pub type CacheFuture<Output> = Pin<Box<(dyn Future<Output = Result<Output, (StatusCode, &'static str)>> + Send + Sync)>>; +pub type CacheGetter<Output> = fn(&String) -> CacheFuture<Output>; +pub type Cache<Output> = Arc<RwLock<AsyncCache<String, Output, CacheGetter<Output>>>>; diff --git a/src/config.rs b/src/config.rs index e635f4a..3c11bc8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,7 +6,8 @@ use std::pin::Pin; use std::time::*; use super::cache::AsyncCache; -use super::deserialize::{GetRecentTracks, GetUserInfo, Track, User}; +use super::deserialize::{GetRecentTracks, GetUserInfo, GetTrackInfo, Track, TrackStub, User}; +use super::font::{font_cache, FontCache}; use reqwest::{Client, StatusCode}; use dotenv::var; @@ -18,13 +19,9 @@ type CacheFuture<Output> = Pin<Box<(dyn Future<Output = Result<Output, (StatusCo type CacheGetter<Output> = fn(&String) -> CacheFuture<Output>; type Cache<Output> = Arc<RwLock<AsyncCache<String, Output, CacheGetter<Output>>>>; -type FontFuture = CacheFuture<Arc<str>>; -type FontGetter = CacheGetter<Arc<str>>; -type FontCache = Cache<Arc<str>>; - -type UserFuture = CacheFuture<Arc<(User, Track)>>; -type UserGetter = CacheGetter<Arc<(User, Track)>>; -type UserCache = Cache<Arc<(User, Track)>>; +type UserFuture = CacheFuture<Arc<(User, Track, TrackStub)>>; +type UserGetter = CacheGetter<Arc<(User, Track, TrackStub)>>; +type UserCache = Cache<Arc<(User, Track, TrackStub)>>; static INTERNAL_THEMES: &[(&str, &str)] = &[("plain", include_str!("themes/plain.hbs"))]; @@ -49,40 +46,22 @@ fn user_getter(username: &String) -> UserFuture { 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 tracksinfo = tracksreq.json::<GetRecentTracks>().await + let trackstub = tracksreq.json::<GetRecentTracks>().await .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!"))?; - Ok(Arc::new((userinfo, tracksinfo))) - }) -} - -fn font_getter(fontname: &String) -> FontFuture { - let fontname = urlencoding::encode(fontname.as_ref()).to_string(); - Box::pin(async move { - let Some(google_api_key) = STATE.google_api_key.clone() - else { - unreachable!(); - }; - - let fontreq = STATE.http.get(format!("https://www.googleapis.com/webfonts/v1/webfonts?key={}&family={fontname}", google_api_key)) + let trackreq = STATE.http.get(format!("https://ws.audioscrobbler.com/2.0/?method=track.getInfo&format=json&username={username}&api_key={}&track={}&artist={}", STATE.lastfm_api_key, trackstub.name, trackstub.artist.name)) .send().await - .map_err(|e| {log::error!("Failed to get info for font `{fontname}`: {e}"); (StatusCode::SERVICE_UNAVAILABLE, "Couldn't connect to Google Fonts!")})?; - if fontreq.status() == StatusCode::NOT_FOUND { return Err((StatusCode::NOT_FOUND, "Font does not exist!")); } - if fontreq.status() == StatusCode::FORBIDDEN { - log::error!("Invalid Google API key in config!"); - return Err((StatusCode::SERVICE_UNAVAILABLE, "This instance is not configured to support Google Fonts properly, please use a different font.")); - } + .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 cssreq = STATE.http.get(format!("https://fonts.googleapis.com/css2?family={fontname}")) - .send().await - .map_err(|e| {log::error!("Failed to get CSS for font `{fontname}`: {e}"); (StatusCode::SERVICE_UNAVAILABLE, "Couldn't download font CSS!")})?; + let trackinfo = trackreq.json::<GetTrackInfo>().await + .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; - Ok(cssreq.text().await.unwrap().into()) + Ok(Arc::new((userinfo, trackinfo, trackstub))) }) } - #[derive(Debug)] enum Whitelist { Exclusive{cache: UserCache, whitelist: BTreeSet<String>}, @@ -95,11 +74,11 @@ pub struct State { default_theme: Arc<str>, send_refresh_header: bool, - http: Client, + pub(crate) http: Client, handlebars: Handlebars<'static>, - google_api_key: Option<Arc<str>>, + pub(crate) google_api_key: Option<Arc<str>>, google_fonts_cache: FontCache, whitelist: Whitelist, @@ -146,7 +125,7 @@ impl State { }, google_api_key: var("LFME_GOOGLE_API_KEY").map(Into::into).ok(), - google_fonts_cache: Arc::new(RwLock::new(AsyncCache::new(Duration::from_secs(86400), font_getter as FontGetter))), + google_fonts_cache: font_cache(), whitelist: { let load_whitelist = || -> Option<BTreeSet<String>> { @@ -178,7 +157,7 @@ impl State { self.google_fonts_cache.write().await.get_owned(font).await } pub fn has_google_api_key(&self) -> bool { self.google_api_key.is_some() } - pub async fn get_userinfo(&self, user: &String) -> (Result<Arc<(User, Track)>, (StatusCode, &'static str)>, Duration) { + pub async fn get_userinfo(&self, user: &String) -> (Result<Arc<(User, Track, TrackStub)>, (StatusCode, &'static str)>, Duration) { match &self.whitelist { Whitelist::Open{default_cache, whitelist_cache, whitelist} => { if whitelist.contains(user) { @@ -40,7 +40,7 @@ pub mod model { pub name: Arc<str>, /// A link to their current image. - pub image_url: Arc<str>, +// pub image_url: Arc<str>, /// A link to their last.fm page. pub url: Arc<str> } @@ -106,10 +106,10 @@ pub mod model { pub struct ResponseCtx(pub model::Root, pub StatusCode); impl ResponseCtx { - pub async fn create(api_result: Result<Arc<(de::User, de::Track)>, (StatusCode, &'static str)>, font_query: Option<FontQuery>, query: BTreeMap<String, String>) -> ResponseCtx { + pub async fn create(api_result: Result<Arc<(de::User, de::Track, de::TrackStub)>, (StatusCode, &'static str)>, font_query: Option<FontQuery>, query: BTreeMap<String, String>) -> ResponseCtx { match api_result { Ok(a) => { - let (user, track) = a.as_ref(); + let (user, track, trackstub) = a.as_ref(); ResponseCtx((model::Data { user: model::User { name: user.name.clone(), @@ -129,11 +129,11 @@ impl ResponseCtx { album: track.album.name.clone(), artist: model::Artist { name: track.artist.name.clone(), - image_url: track.artist.images.iter().max_by(|a, b| a.size.cmp(&b.size)).map(|a| a.url.clone()).unwrap_or_else(|| "".into()), +// image_url: track.artist.images.iter().max_by(|a, b| a.size.cmp(&b.size)).map(|a| a.url.clone()).unwrap_or_else(|| "".into()), url: track.artist.url.clone().unwrap_or_else(|| "".into()) }, - image_url: track.images.iter().max_by(|a, b| a.size.cmp(&b.size)).map(|a| a.url.clone()).unwrap_or_else(|| "".into()), - now_playing: track.attr.nowplaying, + image_url: track.images.iter().max_by(|a, b| a.size.cmp(&b.size)).map(|a| a.url.clone()).filter(|s| !s.is_empty()).unwrap_or_else(|| "https://lastfm.freetls.fastly.net/i/u/128s/4128a6eb29f94943c9d206c08e625904.jpg".into()), + now_playing: trackstub.attr.nowplaying, url: track.url.clone(), loved: track.loved.unwrap_or(false) }, diff --git a/src/deserialize.rs b/src/deserialize.rs index b31fe35..9606258 100644 --- a/src/deserialize.rs +++ b/src/deserialize.rs @@ -62,15 +62,10 @@ pub struct TimeStamp { #[derive(Deserialize, Debug)] pub struct Artist { - #[serde(rename = "mbid")] - pub uuid: Arc<str>, #[serde(alias = "#text")] pub name: Arc<str>, #[serde(default)] - #[serde(rename = "image")] - pub images: Vec<Image>, - #[serde(default)] pub url: Option<Arc<str>> } @@ -97,43 +92,68 @@ pub struct Album { pub uuid: Arc<str>, #[serde(rename = "#text")] pub name: Arc<str>, -} - -#[derive(Default, Deserialize, Debug)] -pub struct TrackAttr { #[serde(default)] - #[serde(deserialize_with = "str_bool")] - pub nowplaying: bool, - #[serde(flatten)] - pub rest: HashMap<Arc<str>, Value>, + #[serde(rename = "image")] + pub images: Vec<Image>, + } + #[derive(Deserialize, Debug)] pub struct Track { pub artist: Artist, - #[serde(deserialize_with = "str_bool")] - pub streamable: bool, #[serde(rename = "image")] pub images: Vec<Image>, - #[serde(rename = "mbid")] - pub uuid: Arc<str>, + pub mbid: Arc<str>, pub album: Album, pub name: Arc<str>, - #[serde(rename = "@attr")] - #[serde(default)] - pub attr: TrackAttr, pub url: Arc<str>, + #[serde(deserialize_with = "str_num")] + pub duration: u64, + #[serde(deserialize_with = "str_num")] + pub listeners: u64, + #[serde(deserialize_with = "str_num")] + pub playcount: u64, #[serde(default)] + #[serde(rename = "userloved")] #[serde(deserialize_with = "str_bool")] pub loved: Option<bool>, - #[serde(default)] - pub date: Option<TimeStamp> + #[serde(deserialize_with = "str_num")] + pub userplaycount: u64, +} + +#[derive(Deserialize, Debug)] +pub struct GetTrackInfo { + pub track: Track } +#[derive(Default, Deserialize, Debug)] +pub struct TrackAttr { + #[serde(default)] + #[serde(deserialize_with = "str_bool")] + pub nowplaying: bool, + #[serde(flatten)] + pub rest: HashMap<Arc<str>, Value>, +} +#[derive(Deserialize, Debug)] +pub struct ArtistStub { + #[serde(rename = "#text")] + pub name: Arc<str> +} +#[derive(Deserialize, Debug)] +pub struct TrackStub { + pub name: Arc<str>, + pub artist: ArtistStub, + #[serde(default)] + pub date: Option<TimeStamp>, + #[serde(rename = "@attr")] + #[serde(default)] + pub attr: TrackAttr, +} #[derive(Deserialize, Debug)] pub struct RecentTracks { - pub track: Vec<Track> + pub track: Vec<TrackStub> } #[derive(Deserialize, Debug)] pub struct GetRecentTracks { diff --git a/src/font.rs b/src/font.rs index ba3bbd9..6454770 100644 --- a/src/font.rs +++ b/src/font.rs @@ -1,4 +1,11 @@ use std::sync::Arc; +use std::time::Duration; + +use tokio::sync::RwLock; +use reqwest::StatusCode; + +use super::cache::{CacheFuture, CacheGetter, Cache, AsyncCache}; +use crate::STATE; #[derive(serde::Deserialize, Debug, Default)] #[serde(default)] @@ -9,3 +16,36 @@ pub struct FontQuery { pub google_font: Option<Arc<str>>, // pub small_font: Option<()> } + +pub type FontFuture = CacheFuture<Arc<str>>; +pub type FontGetter = CacheGetter<Arc<str>>; +pub type FontCache = Cache<Arc<str>>; + +fn font_getter(fontname: &String) -> FontFuture { + let fontname = urlencoding::encode(fontname.as_ref()).to_string(); + Box::pin(async move { + let Some(google_api_key) = STATE.google_api_key.clone() + else { + unreachable!(); + }; + + let fontreq = STATE.http.get(format!("https://www.googleapis.com/webfonts/v1/webfonts?key={}&family={fontname}", google_api_key)) + .send().await + .map_err(|e| {log::error!("Failed to get info for font `{fontname}`: {e}"); (StatusCode::SERVICE_UNAVAILABLE, "Couldn't connect to Google Fonts!")})?; + if fontreq.status() == StatusCode::NOT_FOUND { return Err((StatusCode::NOT_FOUND, "Font does not exist!")); } + if fontreq.status() == StatusCode::FORBIDDEN { + log::error!("Invalid Google API key in config!"); + return Err((StatusCode::SERVICE_UNAVAILABLE, "This instance is not configured to support Google Fonts properly, please use a different font.")); + } + + let cssreq = STATE.http.get(format!("https://fonts.googleapis.com/css2?family={fontname}")) + .send().await + .map_err(|e| {log::error!("Failed to get CSS for font `{fontname}`: {e}"); (StatusCode::SERVICE_UNAVAILABLE, "Couldn't download font CSS!")})?; + + Ok(cssreq.text().await.unwrap().into()) + }) +} + +pub fn font_cache() -> FontCache { + Arc::new(RwLock::new(AsyncCache::new(Duration::from_secs(86400), font_getter as FontGetter))) +} |