M src/cache.rs => src/cache.rs +14 -1
@@ 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>>>>;
M src/config.rs => src/config.rs +16 -37
@@ 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) {
M src/ctx.rs => src/ctx.rs +6 -6
@@ 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)
},
M src/deserialize.rs => src/deserialize.rs +43 -23
@@ 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 {
M src/font.rs => src/font.rs +40 -0
@@ 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)))
+}