aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config.rs21
-rw-r--r--src/ctx.rs12
-rw-r--r--src/main.rs26
-rw-r--r--src/themes/plain.hbs34
4 files changed, 73 insertions, 20 deletions
diff --git a/src/config.rs b/src/config.rs
index aad298f..ab92108 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -11,17 +11,17 @@ use super::deserialize::{GetRecentTracks, GetUserInfo, Track, User};
use reqwest::{Client, StatusCode};
use dotenv::var;
use tokio::sync::RwLock;
-use handlebars::Handlebars;
+use handlebars::{Handlebars, handlebars_helper};
use duration_str as ds;
-static INTERNAL_THEMES: &[(&'static str, &'static str)] = &[("plain", "")];
+static INTERNAL_THEMES: &[(&'static str, &'static str)] = &[("plain", include_str!("themes/plain.hbs"))];
pub static STATE: LazyLock<Arc<State>> = LazyLock::new(|| {
State::new()
});
fn getter(username: &String) -> Pin<Box<(dyn Future<Output = Result<Arc<(User, Track)>, (StatusCode, &'static str)>> + Send + Sync)>> {
- let username = username.clone();
+ let username = urlencoding::encode(username.as_ref()).to_string();
Box::pin(async move {
let userreq = STATE.http.get(format!("https://ws.audioscrobbler.com/2.0/?method=user.getInfo&format=json&user={username}&api_key={}", STATE.api_key))
.send().await
@@ -84,6 +84,15 @@ impl State {
handlebars: {
let mut hb = Handlebars::new();
+
+ handlebars_helper!(html_escape: |s: String| htmlize::escape_text(s));
+ handlebars_helper!(html_attr_escape: |s: String| htmlize::escape_attribute(s));
+ handlebars_helper!(url_encode: |s: String| urlencoding::encode(&s));
+
+ hb.register_helper("html-escape", Box::new(html_escape));
+ hb.register_helper("html-attr-escape", Box::new(html_attr_escape));
+ hb.register_helper("url-encode", Box::new(html_attr_escape));
+
for (key, fulltext) in INTERNAL_THEMES {
log::info!(target: "lfm::config::theme", "Registering internal theme `{key}`");
hb.register_template_string(key, fulltext).unwrap();
@@ -92,7 +101,7 @@ impl State {
if let Ok(themes_dir) = var("LFME_THEME_DIR") {
log::info!(target: "lfm::config::theme", "Registering theme dir `{themes_dir}`");
- hb.register_templates_directory(&var("LFME_THEME_EXT").unwrap_or_else(|_| "hbs".into()), themes_dir).unwrap();
+ hb.register_templates_directory(&var("LFME_THEME_EXT").unwrap_or_else(|_| ".hbs".into()), themes_dir).unwrap();
}
hb
@@ -118,8 +127,8 @@ impl State {
}
}
},
- default_refresh: default_refresh + Duration::from_secs(5),
- whitelist_refresh: whitelist_refresh + Duration::from_secs(5)
+ default_refresh: default_refresh + Duration::from_secs(1),
+ whitelist_refresh: whitelist_refresh + Duration::from_secs(1)
})
}
diff --git a/src/ctx.rs b/src/ctx.rs
index 6beb634..63b54ff 100644
--- a/src/ctx.rs
+++ b/src/ctx.rs
@@ -1,9 +1,11 @@
use reqwest::StatusCode;
use super::deserialize as de;
use std::sync::Arc;
+use std::collections::BTreeMap;
pub mod model {
use std::sync::Arc;
+ use std::collections::BTreeMap;
/// The theme representation of a user.
#[derive(serde::Serialize, Debug)]
@@ -73,14 +75,15 @@ pub mod model {
/// Contains text explaining a potential error.
Error { error: &'static str },
/// Contains data about a user and what they're listening to.
- Data { user: User, scrobble: Scrobble }
+ Data { user: User, scrobble: Scrobble, query: BTreeMap<String, String> }
}
}
#[derive(Debug)]
pub struct ResponseCtx(pub model::Data, pub StatusCode);
-impl From<Result<Arc<(de::User, de::Track)>, (StatusCode, &'static str)>> for ResponseCtx {
- fn from(v: Result<Arc<(de::User, de::Track)>, (StatusCode, &'static str)>) -> ResponseCtx {
+impl From<(Result<Arc<(de::User, de::Track)>, (StatusCode, &'static str)>, BTreeMap<String, String>)> for ResponseCtx {
+ fn from(v: (Result<Arc<(de::User, de::Track)>, (StatusCode, &'static str)>, BTreeMap<String, String>)) -> ResponseCtx {
+ let (v, q) = v;
match v {
Ok(a) => {
let (user, track) = a.as_ref();
@@ -111,7 +114,8 @@ impl From<Result<Arc<(de::User, de::Track)>, (StatusCode, &'static str)>> for Re
now_playing: track.attr.nowplaying,
url: track.url.clone(),
loved: track.loved.unwrap_or(false)
- }
+ },
+ query: q
}, StatusCode::OK)
},
Err((status, error)) => {
diff --git a/src/main.rs b/src/main.rs
index 729482f..b787049 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,6 +2,7 @@
use dotenv::var;
use log::LevelFilter;
+use std::collections::BTreeMap;
use std::fs::File;
use std::sync::Arc;
use lfm_embed::{STATE, ResponseCtx};
@@ -10,7 +11,9 @@ use warp::Filter;
#[derive(serde::Deserialize, Debug)]
struct UserQuery {
#[serde(default)]
- theme: Option<Arc<str>>
+ theme: Option<Arc<str>>,
+ #[serde(flatten)]
+ rest: BTreeMap<String, String>
}
#[tokio::main]
@@ -35,17 +38,20 @@ async fn main() {
let user = warp::path!("user" / String)
.and(warp::query::<UserQuery>())
.then(|s, q: UserQuery| async move {
- log::info!(target: "lfm::server::user", "Handling request for user `{s}` with {q:?}");
+ log::debug!(target: "lfm::server::user", "Handling request for user `{s}` with {q:?}");
let (ctx, dur) = STATE.get_userinfo(&s).await;
- let ResponseCtx(data, status) = ctx.into();
-
- let theme = q.theme.unwrap_or_else(|| STATE.default_theme());
+ let ResponseCtx(mut data, status) = (ctx, q.rest).into();
+
+ let theme = q.theme.filter(|a| STATE.handlebars().has_template(&a)).unwrap_or_else(|| STATE.default_theme());
+ log::debug!(target: "lfm::server::user", "Using theme {theme}");
warp::reply::with_header(
- warp::reply::with_status(
- warp::reply::html(
- STATE.handlebars().render(&theme, &data).unwrap()
- ), status
- ), "Refresh", dur.as_secs()
+ warp::reply::with_header(
+ warp::reply::with_status(
+ warp::reply::html(
+ STATE.handlebars().render(&theme, &data).unwrap()
+ ), status
+ ), "Refresh", dur.as_secs()
+ ), "X-Selected-Theme", theme.as_ref()
)
});
diff --git a/src/themes/plain.hbs b/src/themes/plain.hbs
new file mode 100644
index 0000000..cbe8948
--- /dev/null
+++ b/src/themes/plain.hbs
@@ -0,0 +1,34 @@
+<!doctype html>
+<html lang="en" style="font-size: 0.5cm; margin: 0.5rem; overflow: hidden;">
+ <head>
+ <meta charset="UTF-8"/>
+ <title>{{#if error}}Error!{{else}}@{{user.name}}'s Last.fm Stats{{/if}}</title>
+ <style>
+ {{#if (eq query.dark null)}}
+ :root { --b: black; color: black; backgrond-color: white; }
+ {{else}}
+ :root { --b: white; color: white; background-color: black; }
+ a:visited { color: pink }
+ a { color: cyan; }
+ {{/if}}
+ p { margin: 0px; padding: 0px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
+ </style>
+ </head>
+ <body>
+ {{#if error}}
+ <p style="white-space: unset;">{{error}}</p>
+ {{else}}
+ <a style="float: left;" target="_blank" href="{{scrobble.url}}"><img src="{{scrobble.image_url}}" style="height: 4.0rem; border: solid var(--b) 0.2rem; margin: 0.1rem;" /></a>
+ <p><a target="_blank" href="{{user.url}}">@{{user.name}}</a>{{#if scrobble.now_playing}}
+ is scrobbling{{else}}'s last scrobble was
+ {{/if}}
+ </p><p>
+ <i><b><a target="_blank" href="{{scrobble.url}}">{{scrobble.name}}</a></b></i>
+ </p><p>
+ {{#if scrobble.album}}from <i><b>{{scrobble.album}}</b></i>{{/if}}
+ </p><p>
+ by <b><a target="_blank" href="{{scrobble.artist.url}}">{{scrobble.artist.name}}</a></b>.
+ </p>
+ {{/if}}
+ </body>
+</html> \ No newline at end of file