1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
|
// 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)
}
}
}
|