From 22c2e4e2db9ad9d892ed5fb63d92254677f6dafd Mon Sep 17 00:00:00 2001 From: alyx Date: Mon, 1 Apr 2024 19:00:57 -0400 Subject: 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. --- src/cache.rs | 15 ++++++++++++- src/config.rs | 53 +++++++++++++------------------------------ src/ctx.rs | 12 +++++----- src/deserialize.rs | 66 +++++++++++++++++++++++++++++++++++------------------- src/font.rs | 40 +++++++++++++++++++++++++++++++++ 5 files changed, 119 insertions(+), 67 deletions(-) (limited to 'src') 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 { func: F, @@ -66,3 +75,7 @@ where self.get(key).await.cloned() } } + +pub type CacheFuture = Pin> + Send + Sync)>>; +pub type CacheGetter = fn(&String) -> CacheFuture; +pub type Cache = Arc>>>; 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 = Pin = fn(&String) -> CacheFuture; type Cache = Arc>>>; -type FontFuture = CacheFuture>; -type FontGetter = CacheGetter>; -type FontCache = Cache>; - -type UserFuture = CacheFuture>; -type UserGetter = CacheGetter>; -type UserCache = Cache>; +type UserFuture = CacheFuture>; +type UserGetter = CacheGetter>; +type UserCache = Cache>; 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::().await + let trackstub = tracksreq.json::().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::().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}, @@ -95,11 +74,11 @@ pub struct State { default_theme: Arc, send_refresh_header: bool, - http: Client, + pub(crate) http: Client, handlebars: Handlebars<'static>, - google_api_key: Option>, + pub(crate) google_api_key: Option>, 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> { @@ -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, (StatusCode, &'static str)>, Duration) { + pub async fn get_userinfo(&self, user: &String) -> (Result, (StatusCode, &'static str)>, Duration) { match &self.whitelist { Whitelist::Open{default_cache, whitelist_cache, whitelist} => { if whitelist.contains(user) { diff --git a/src/ctx.rs b/src/ctx.rs index d564bd6..0b250b1 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -40,7 +40,7 @@ pub mod model { pub name: Arc, /// A link to their current image. - pub image_url: Arc, +// pub image_url: Arc, /// A link to their last.fm page. pub url: Arc } @@ -106,10 +106,10 @@ pub mod model { pub struct ResponseCtx(pub model::Root, pub StatusCode); impl ResponseCtx { - pub async fn create(api_result: Result, (StatusCode, &'static str)>, font_query: Option, query: BTreeMap) -> ResponseCtx { + pub async fn create(api_result: Result, (StatusCode, &'static str)>, font_query: Option, query: BTreeMap) -> 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,14 +62,9 @@ pub struct TimeStamp { #[derive(Deserialize, Debug)] pub struct Artist { - #[serde(rename = "mbid")] - pub uuid: Arc, #[serde(alias = "#text")] pub name: Arc, - #[serde(default)] - #[serde(rename = "image")] - pub images: Vec, #[serde(default)] pub url: Option> } @@ -97,43 +92,68 @@ pub struct Album { pub uuid: Arc, #[serde(rename = "#text")] pub name: Arc, -} - -#[derive(Default, Deserialize, Debug)] -pub struct TrackAttr { #[serde(default)] - #[serde(deserialize_with = "str_bool")] - pub nowplaying: bool, - #[serde(flatten)] - pub rest: HashMap, Value>, + #[serde(rename = "image")] + pub images: Vec, + } + #[derive(Deserialize, Debug)] pub struct Track { pub artist: Artist, - #[serde(deserialize_with = "str_bool")] - pub streamable: bool, #[serde(rename = "image")] pub images: Vec, - #[serde(rename = "mbid")] - pub uuid: Arc, + pub mbid: Arc, pub album: Album, pub name: Arc, - #[serde(rename = "@attr")] - #[serde(default)] - pub attr: TrackAttr, pub url: Arc, + #[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, - #[serde(default)] - pub date: Option + #[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, Value>, +} +#[derive(Deserialize, Debug)] +pub struct ArtistStub { + #[serde(rename = "#text")] + pub name: Arc +} +#[derive(Deserialize, Debug)] +pub struct TrackStub { + pub name: Arc, + pub artist: ArtistStub, + #[serde(default)] + pub date: Option, + #[serde(rename = "@attr")] + #[serde(default)] + pub attr: TrackAttr, +} #[derive(Deserialize, Debug)] pub struct RecentTracks { - pub track: Vec + pub track: Vec } #[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>, // pub small_font: Option<()> } + +pub type FontFuture = CacheFuture>; +pub type FontGetter = CacheGetter>; +pub type FontCache = Cache>; + +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))) +} -- cgit v1.2.3-54-g00ecf