~aleteoryx/lfm_embed

41a5fa81b5c12feb8aab0ad82d42c135e93065f7 — alyx 9 months ago be166d9
Add custom font support.
7 files changed, 61 insertions(+), 10 deletions(-)

M README.md
M src/config.rs
M src/ctx.rs
A src/font.rs
M src/lib.rs
M src/main.rs
M src/themes/plain.hbs
M README.md => README.md +18 -2
@@ 160,7 160,15 @@ Themes should have, roughly, the structure below:
<html lang="en"> <!-- Or really whatever you want, but I speak English. -->
    <head>
        <meta charset="UTF-8">
        <style> /* styles */ </style>
        <style>
          {{#if font.css}}
            {{{font.css}}}
          {{/if}}
          {{#if (or font.name font.css)}}
            * { font-family: '{{#if font.name}}{{font.name}}{{/if}}{{#if font.url}}included_font{{/if}}' }
          {{/if}}
          /* actual styles */
        </style>
    </head>
    <body>
        {{#if error}}<p>{{error}}</p>{{else}}


@@ 225,7 233,15 @@ If there was an error, the object will just be `{ error: String }`, otherwise it
        // the most recently played track.
        now_playing: Boolean,
    },
    

    // Custom font info.
    font: {
        // Set if the theme should replace the custom font with something.
        name: String?
        // Will contain CSS which includes a custom font as 'included_font'.
        css: String?
    }

    // A set of extraneous query parameters.
    //
    // This should be considered UNTRUSTED, as the requester has full

M src/config.rs => src/config.rs +1 -1
@@ 62,7 62,7 @@ pub struct State {
    http: Client,

    default_refresh: Duration,
    whitelist_refresh: Duration
    whitelist_refresh: Duration,
}

impl State {

M src/ctx.rs => src/ctx.rs +17 -4
@@ 2,6 2,7 @@ use reqwest::StatusCode;
use super::deserialize as de;
use std::sync::Arc;
use std::collections::BTreeMap;
use super::font::FontQuery;

pub mod model {
    use std::sync::Arc;


@@ 66,6 67,13 @@ pub mod model {
        pub loved: bool
    }

    #[derive(serde::Serialize, Debug)]
    #[serde(untagged)]
    pub enum Font {
        Url { css: Arc<str> },
        Name { name: Arc<str> },
    }

    /// 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.


@@ 75,15 83,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, query: BTreeMap<String, String> }
        Data { user: User, scrobble: Scrobble, font: Option<Font>, 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)>, 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;
impl From<(Result<Arc<(de::User, de::Track)>, (StatusCode, &'static str)>, Option<FontQuery>, BTreeMap<String, String>)> for ResponseCtx {
    fn from(v: (Result<Arc<(de::User, de::Track)>, (StatusCode, &'static str)>, Option<FontQuery>, BTreeMap<String, String>)) -> ResponseCtx {
        let (v, f, q) = v;
        match v {
            Ok(a) => {
                let (user, track) = a.as_ref();


@@ 115,6 123,11 @@ impl From<(Result<Arc<(de::User, de::Track)>, (StatusCode, &'static str)>, BTree
                        url: track.url.clone(),
                        loved: track.loved.unwrap_or(false)
                    },
                    font: match f {
                        Some(FontQuery { font: Some(s), include_font: None }) => Some(model::Font::Name { name: s }),
                        Some(FontQuery { include_font: Some(s), .. }) => Some(model::Font::Url { css: format!("@font-face {{ font-family: 'included_font'; src: url('{}'); }}", s.replace("\\", "\\\\").replace("'", "\\'")).into() }),
                        _ => None,
                    },
                    query: q
                }, StatusCode::OK)
            },

A src/font.rs => src/font.rs +10 -0
@@ 0,0 1,10 @@
use std::sync::Arc;

#[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 small_font: Option<()>
}

M src/lib.rs => src/lib.rs +1 -0
@@ 4,6 4,7 @@ pub mod deserialize;
pub mod cache;
pub mod config;
pub mod ctx;
pub mod font;

pub use config::STATE;
pub use ctx::ResponseCtx;

M src/main.rs => src/main.rs +5 -1
@@ 6,6 6,7 @@ use std::collections::BTreeMap;
use std::fs::File;
use std::sync::Arc;
use lfm_embed::{STATE, ResponseCtx};
use lfm_embed::font::FontQuery;
use warp::Filter;

#[derive(serde::Deserialize, Debug)]


@@ 13,6 14,9 @@ struct UserQuery {
    #[serde(default)]
    theme: Option<Arc<str>>,
    #[serde(flatten)]
    #[serde(default)]
    font: Option<FontQuery>,
    #[serde(flatten)]
    rest: BTreeMap<String, String>
}



@@ 40,7 44,7 @@ async fn main() {
        .then(|s, q: UserQuery| async move {
            log::debug!(target: "lfm::server::user", "Handling request for user `{s}` with {q:?}");
            let (ctx, dur) = STATE.get_userinfo(&s).await;
            let ResponseCtx(mut data, status) = (ctx, q.rest).into();
            let ResponseCtx(mut data, status) = (ctx, q.font, 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}");

M src/themes/plain.hbs => src/themes/plain.hbs +9 -2
@@ 1,5 1,5 @@
<!doctype html>
<html lang="en" style="font-size: 0.5cm; margin: 0.5rem; overflow: hidden;">
<html lang="en" style="margin: 20px; overflow: hidden;">
  <head>
    <meta charset="UTF-8"/>
    <title>{{#if error}}Error!{{else}}@{{user.name}}'s Last.fm Stats{{/if}}</title>


@@ 11,6 11,13 @@
        a:visited { color: pink }
        a { color: cyan; }
      {{/if}}
      * { font-size: {{#if font}}17px{{else}}20px{{/if}}; }
      {{#if font.css}}
        {{{font.css}}}
      {{/if}}
      {{#if (or font.name font.css)}}
        * { font-family: '{{#if font.name}}{{font.name}}{{/if}}{{#if font.url}}included_font{{/if}}' }
      {{/if}}
      p { margin: 0px; padding: 0px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
    </style>
  </head>


@@ 18,7 25,7 @@
    {{#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>
      <a style="float: left;" target="_blank" href="{{scrobble.url}}"><img src="{{scrobble.image_url}}" style="height: 80px; border: solid var(--b) 4px; margin: 2px;" /></a>
      <p><a target="_blank" href="{{user.url}}">@{{user.name}}</a>{{#if scrobble.now_playing}}
        is scrobbling{{else}}'s last scrobble was
      {{/if}}