aboutsummaryrefslogtreecommitdiffstats
path: root/src/cache
diff options
context:
space:
mode:
authoralyx <alyx@aleteoryx.me>2024-04-01 20:27:44 -0400
committeralyx <alyx@aleteoryx.me>2024-04-01 20:27:44 -0400
commit83ba4fe37a1184b998be09b9cbe53a22c7ba9e3b (patch)
treec60c2c17caeed24db920dee4613c1e185b12d12b /src/cache
parent22c2e4e2db9ad9d892ed5fb63d92254677f6dafd (diff)
downloadlfm_embed-83ba4fe37a1184b998be09b9cbe53a22c7ba9e3b.tar.gz
lfm_embed-83ba4fe37a1184b998be09b9cbe53a22c7ba9e3b.tar.bz2
lfm_embed-83ba4fe37a1184b998be09b9cbe53a22c7ba9e3b.zip
Move caching to src/cache/; Finalize API parsing fixes
Font and user cache code has been moved to special files, independant from src/config.rs API parsing changes have been properly tested, and last.fm API JSON is now trace-logged for debugging convenience.
Diffstat (limited to 'src/cache')
-rw-r--r--src/cache/font.rs58
-rw-r--r--src/cache/user.rs101
2 files changed, 159 insertions, 0 deletions
diff --git a/src/cache/font.rs b/src/cache/font.rs
new file mode 100644
index 0000000..c17247d
--- /dev/null
+++ b/src/cache/font.rs
@@ -0,0 +1,58 @@
+use std::sync::Arc;
+use std::time::Duration;
+use std::sync::LazyLock;
+
+use tokio::sync::RwLock;
+use reqwest::{StatusCode, Client};
+
+use super::{CacheFuture, CacheGetter, Cache, AsyncCache};
+use crate::CONFIG;
+
+#[derive(serde::Deserialize, Debug, Default)]
+#[serde(default)]
+#[serde(rename = "kebab-case")]
+pub struct FontQuery {
+ pub font: Option<Arc<str>>,
+ pub include_font: Option<Arc<str>>,
+ 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) = CONFIG.google_api_key.clone()
+ else {
+ unreachable!();
+ };
+
+ let fontreq = 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 = 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())
+ })
+}
+
+static HTTP: LazyLock<Client> = crate::http::lazy();
+
+static FONT_CACHE: LazyLock<FontCache> = LazyLock::new(|| {
+ Arc::new(RwLock::new(AsyncCache::new(Duration::from_secs(86400), font_getter as FontGetter)))
+});
+
+pub async fn get_fontinfo(font: &String) -> Result<Arc<str>, (StatusCode, &'static str)> {
+ FONT_CACHE.write().await.get_owned(font).await
+}
diff --git a/src/cache/user.rs b/src/cache/user.rs
new file mode 100644
index 0000000..c3b6f04
--- /dev/null
+++ b/src/cache/user.rs
@@ -0,0 +1,101 @@
+use std::sync::Arc;
+use std::time::Duration;
+use std::sync::LazyLock;
+
+use tokio::sync::RwLock;
+use reqwest::{StatusCode, Client};
+
+use super::{CacheFuture, CacheGetter, Cache, AsyncCache};
+use crate::deserialize::{User, Track, TrackStub};
+use crate::CONFIG;
+
+type UserFuture = CacheFuture<Arc<(User, Track, TrackStub)>>;
+type UserGetter = CacheGetter<Arc<(User, Track, TrackStub)>>;
+type UserCache = Cache<Arc<(User, Track, TrackStub)>>;
+
+#[derive(Debug)]
+enum Whitelist {
+ Exclusive{cache: UserCache},
+ Open{default_cache: UserCache, whitelist_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::<GetUserInfo>(&userstr)
+ .map_err(|e| {log::error!("Couldn't parse user.getInfo for `{username}`: {e}"); (StatusCode::INTERNAL_SERVER_ERROR, "Couldn't parse user.getInfo!")})?.user;
+
+ 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::<GetRecentTracks>(&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!"))?;
+
+ 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, trackstub.name, 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 user.getTrackInfo JSON for `{username}`: {trackstr}");
+ let trackinfo = serde_json::from_str::<GetTrackInfo>(&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;
+
+ Ok(Arc::new((userinfo, trackinfo, trackstub)))
+ })
+}
+
+static HTTP: LazyLock<Client> = crate::http::lazy();
+
+static WHITELIST: LazyLock<Whitelist> = LazyLock::new(|| {
+ let default_cache = Arc::new(RwLock::new(AsyncCache::new(CONFIG.default_refresh, user_getter as UserGetter)));
+ let whitelist_cache = Arc::new(RwLock::new(AsyncCache::new(CONFIG.whitelist_refresh, user_getter as UserGetter)));
+ match CONFIG.whitelist_mode.as_str() {
+ "open" => {
+ Whitelist::Open{default_cache, whitelist_cache}
+ },
+ "exclusive" => {
+ if CONFIG.whitelist.is_empty() {
+ panic!("Exclusive mode set with empty whitelist, cannot serve any requests!");
+ }
+ Whitelist::Exclusive{cache: whitelist_cache}
+ },
+ m => {
+ panic!("Bad whitelist mode: `{m}`");
+ }
+ }
+});
+
+pub async fn get_userinfo(user: &String) -> (Result<Arc<(User, Track, TrackStub)>, (StatusCode, &'static str)>, Duration) {
+ match LazyLock::force(&WHITELIST) {
+ Whitelist::Open{default_cache, whitelist_cache} => {
+ if CONFIG.whitelist.contains(user) {
+ (whitelist_cache.write().await.get_owned(user).await, CONFIG.whitelist_refresh)
+ }
+ else {
+ (default_cache.write().await.get_owned(user).await, CONFIG.default_refresh)
+ }
+ },
+ Whitelist::Exclusive{cache} => {
+ if CONFIG.whitelist.contains(user) {
+ (cache.write().await.get_owned(user).await, CONFIG.whitelist_refresh)
+ }
+ else { (Err((StatusCode::FORBIDDEN, "User not in whitelist!")), CONFIG.default_refresh) }
+ }
+ }
+}