// SPDX-License-Identifier: AGPL-3.0-only
use std::collections::BTreeMap;
use reqwest::StatusCode;
use super::cache::user::UserInfo;
use super::config::CONFIG;
use crate::cache::font::FontQuery;
pub mod model {
use std::sync::Arc;
use std::collections::BTreeMap;
/// The theme representation of a user.
#[derive(serde::Serialize, Debug)]
pub struct User {
/// Their username.
pub name: Arc<str>,
/// Their "Display Name".
pub realname: Arc<str>,
/// Total scrobbles.
pub scrobble_count: u64,
/// Number of artists in library.
pub artist_count: u64,
/// Number of tracks in library.
pub track_count: u64,
/// Number of albums in library.
pub album_count: u64,
/// Link to user's profile picture.
pub image_url: Arc<str>,
/// Link to user's profile.
pub url: Arc<str>
}
/// The theme representation of an artist
#[derive(serde::Serialize, Debug)]
pub struct Artist {
/// The artist's name.
pub name: Arc<str>,
/// A link to their current image.
// pub image_url: Arc<str>,
/// A link to their last.fm page.
pub url: Arc<str>
}
/// The theme representation of a user's most recently scrobbled track.
#[derive(serde::Serialize, Debug)]
pub struct Scrobble {
/// The name of the track.
pub name: Arc<str>,
/// The name of its album.
pub album: Option<Arc<str>>,
/// The artist who made it.
pub artist: Artist,
/// A link to the track image.
pub image_url: Arc<str>,
/// True if the user is currently scrobbling it, false if it's just the most recently played track.
pub now_playing: bool,
/// A link to the track's last.fm page.
pub url: Arc<str>,
/// True if the user has loved the track.
pub loved: bool
}
/// The user-specified font request parameters
#[derive(serde::Serialize, Debug)]
#[serde(untagged)]
pub enum Font {
/// A font that requires additional CSS to load properly.
External { css: Arc<str>, name: Arc<str> },
/// A font that is w3c standard, or widely installed.
Name { name: Arc<str> },
}
#[derive(serde::Serialize, Debug)]
pub struct Data {
pub user: User,
pub scrobble: Scrobble,
pub font: Option<Font>,
pub query: BTreeMap<String, String>,
}
/// The context passed in to all themes.
///
/// Serialized as untagged, so themes should check if `error` is set and decide whether to show an error state or try rendering user info.
#[derive(serde::Serialize, Debug)]
#[serde(untagged)]
pub enum Root {
/// Contains text explaining a potential error.
Error { error: &'static str },
/// Contains data about a user and what they're listening to.
Data { #[serde(flatten)] data: Box<Data> }
}
impl From<Data> for Root {
fn from(v: Data) -> Self {
Self::Data { data: Box::new(v) }
}
}
}
pub use model::Root as Ctx;
pub async fn get_ctx(api_result: Result<UserInfo, (StatusCode, &'static str)>, font_query: Option<FontQuery>, query: BTreeMap<String, String>) -> (Ctx, StatusCode) {
match api_result {
Ok(a) => {
let (user, track, trackstub) = a.as_ref();
((model::Data {
user: model::User {
name: user.name.clone(),
realname: user.realname.clone(),
scrobble_count: user.playcount,
artist_count: user.artist_count,
track_count: user.track_count,
album_count: user.track_count,
image_url: user.images.iter().max_by(|a, b| a.size.cmp(&b.size)).map(|a| a.url.clone()).unwrap_or_else(|| "".into()),
url: user.url.clone()
},
scrobble: model::Scrobble {
name: track.name.clone(),
album: track.album.as_ref().map(|a| a.title.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()),
url: track.artist.url.clone().unwrap_or_else(|| "".into())
},
image_url: {
let image_url = track.images.iter()
.chain(track.album.iter().flat_map(|x| &x.images))
.chain(&trackstub.images)
.chain(trackstub.album.iter().flat_map(|x| &x.images))
.inspect(|i| log::trace!("got: {i:?}"))
.max_by_key(|a| a.size)
.map(|a| a.url.clone())
.unwrap_or_else(|| "https://lastfm.freetls.fastly.net/i/u/128s/4128a6eb29f94943c9d206c08e625904.jpg".into());
image_url
},
now_playing: trackstub.attr.nowplaying,
url: track.url.clone(),
loved: track.loved.unwrap_or(false)
},
font: match font_query {
Some(FontQuery { google_font: Some(f), .. }) if CONFIG.has_google_api_key() => {
let css = match crate::cache::font::get_fontinfo(&f.to_string()).await {
Ok(css) => css,
Err((status, error)) => { return (model::Root::Error {error}, status); }
};
Some(model::Font::External {
css,
name: f
})
},
Some(FontQuery { include_font: Some(f), .. }) => Some(
model::Font::External {
css: format!(
"@font-face {{ font-family: 'included_font'; src: url('{}'); }}",
f.replace('\\', "\\\\")
.replace('\'', "\\'")).into(),
name: "included_font".into()
}),
Some(FontQuery { font: Some(s), .. }) => Some(model::Font::Name { name: s }),
_ => None,
},
query
}).into(), StatusCode::OK)
},
Err((status, error)) => {
(model::Root::Error {error}, status)
}
}
}