~aleteoryx/lfm_embed

2feec01f58ce903d07d53bb1b79dc6b448b1a146 — alyx 8 months ago d2c27e1
Clippy
4 files changed, 46 insertions(+), 23 deletions(-)

M src/cache.rs
M src/config.rs
M src/ctx.rs
M src/main.rs
M src/cache.rs => src/cache.rs +4 -4
@@ 21,17 21,17 @@ where
  }

  pub async fn get(&mut self, key: &K) -> Result<&V, (StatusCode, &'static str)> {
    if self.is_stale(&key) {
    if self.is_stale(key) {
      log::trace!(target: "lfm::cache", "MISS : interval = {:?}", self.interval);
      self.renew(&key).await
      self.renew(key).await
    } else {
      log::trace!(target: "lfm::cache", "HIT : interval = {:?}", self.interval);
      Ok(&self.cache.get(&key).unwrap().1)
      Ok(&self.cache.get(key).unwrap().1)
    }
  }

  pub async fn renew(&mut self, key: &K) -> Result<&V, (StatusCode, &'static str)> {
    let val = (self.func)(&key).await?;
    let val = (self.func)(key).await?;
    self.cache.insert(key.clone(), (Instant::now(), val));
    Ok(&self.cache.get(key).unwrap().1)
  }

M src/config.rs => src/config.rs +18 -9
@@ 14,13 14,25 @@ use tokio::sync::RwLock;
use handlebars::{Handlebars, handlebars_helper};
use duration_str as ds;

static INTERNAL_THEMES: &[(&'static str, &'static str)] = &[("plain", include_str!("themes/plain.hbs"))];
type CacheFuture<Output> = Pin<Box<(dyn Future<Output = Result<Output, (StatusCode, &'static str)>> + Send + Sync)>>;
type CacheGetter<Output> = fn(&String) -> CacheFuture<Output>;
type Cache<Output>       = Arc<RwLock<AsyncCache<String, Output, CacheGetter<Output>>>>;

type FontFuture = CacheFuture<Arc<str>>;
type FontGetter = CacheGetter<Arc<str>>;
type FontCache  = Cache<Arc<str>>;

type UserFuture = CacheFuture<Arc<(User, Track)>>;
type UserGetter = CacheGetter<Arc<(User, Track)>>;
type UserCache  = Cache<Arc<(User, Track)>>;

static INTERNAL_THEMES: &[(&str, &str)] = &[("plain", include_str!("themes/plain.hbs"))];

pub static STATE: LazyLock<Arc<State>> = LazyLock::new(|| {
  State::new()
});

fn user_getter(username: &String) -> Pin<Box<(dyn Future<Output = Result<Arc<(User, Track)>, (StatusCode, &'static str)>> + Send + Sync)>> {
fn user_getter(username: &String) -> UserFuture {
  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.lastfm_api_key))


@@ 39,13 51,13 @@ fn user_getter(username: &String) -> Pin<Box<(dyn Future<Output = Result<Arc<(Us

    let tracksinfo = tracksreq.json::<GetRecentTracks>().await
      .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().nth(0).ok_or((StatusCode::UNPROCESSABLE_ENTITY, "You need to listen to some songs first!"))?;
      .recenttracks.track.into_iter().next().ok_or((StatusCode::UNPROCESSABLE_ENTITY, "You need to listen to some songs first!"))?;

    Ok(Arc::new((userinfo, tracksinfo)))
  })
}

fn font_getter(fontname: &String) -> Pin<Box<(dyn Future<Output = Result<Arc<str>, (StatusCode, &'static str)>> + Send + Sync)>> {
fn font_getter(fontname: &String) -> FontFuture {
  let fontname = urlencoding::encode(fontname.as_ref()).to_string();
  Box::pin(async move {
    let Some(google_api_key) = STATE.google_api_key.clone()


@@ 70,10 82,7 @@ fn font_getter(fontname: &String) -> Pin<Box<(dyn Future<Output = Result<Arc<str
  })
}

type UserGetter = fn(&String) -> Pin<Box<(dyn Future<Output = Result<Arc<(User, Track)>, (StatusCode, &'static str)>> + Send + Sync)>>;
type UserCache = Arc<RwLock<AsyncCache<String, Arc<(User, Track)>, UserGetter>>>;
type FontGetter = fn(&String) -> Pin<Box<(dyn Future<Output = Result<Arc<str>, (StatusCode, &'static str)>> + Send + Sync)>>;
type FontCache = Arc<RwLock<AsyncCache<String, Arc<str>, FontGetter>>>;

#[derive(Debug)]
enum Whitelist {
  Exclusive{cache: UserCache, whitelist: BTreeSet<String>},


@@ 142,7 151,7 @@ impl State {
      whitelist: {
        let load_whitelist = || -> Option<BTreeSet<String>> {
          var("LFME_WHITELIST").ok().map(
            |w| w.split(",").map(|s| s.trim().to_string()).collect()
            |w| w.split(',').map(|s| s.trim().to_string()).collect()
          )
        };


M src/ctx.rs => src/ctx.rs +23 -9
@@ 76,27 76,41 @@ pub mod model {
    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 Data {
  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 { user: User, scrobble: Scrobble, font: Option<Font>, query: BTreeMap<String, String>,  }
    Data { #[serde(flatten)] data: Box<Data> }
  }

  impl From<Data> for Root {
    fn from(v: Data) -> Self {
      Self::Data { data: Box::new(v) }
    }
  }
}
#[derive(Debug)]
pub struct ResponseCtx(pub model::Data, pub StatusCode);
pub struct ResponseCtx(pub model::Root, pub StatusCode);

impl ResponseCtx {
  pub async fn create(api_result: Result<Arc<(de::User, de::Track)>, (StatusCode, &'static str)>, font_query: Option<FontQuery>, query: BTreeMap<String, String>) -> ResponseCtx {
    match api_result {
      Ok(a) => {
        let (user, track) = a.as_ref();
        ResponseCtx(model::Data::Data {
        ResponseCtx((model::Data {
          user: model::User {
            name: user.name.clone(),
            realname: user.realname.clone(),


@@ 127,7 141,7 @@ impl ResponseCtx {
            Some(FontQuery { google_font: Some(f), .. }) if STATE.has_google_api_key() => {
              let css = match STATE.get_fontinfo(&f.to_string()).await {
                Ok(css) => css,
                Err((status, error)) => { return ResponseCtx(model::Data::Error {error}, status); }
                Err((status, error)) => { return ResponseCtx(model::Root::Error {error}, status); }
              };
              Some(model::Font::External {
                css,


@@ 138,18 152,18 @@ impl ResponseCtx {
              model::Font::External {
                css: format!(
                  "@font-face {{ font-family: 'included_font'; src: url('{}'); }}",
                  f.replace("\\", "\\\\")
                   .replace("'", "\\'")).into(),
                  f.replace('\\', "\\\\")
                   .replace('\'', "\\'")).into(),
                name: "included_font".into()
            }),
            Some(FontQuery { font: Some(s), .. }) => Some(model::Font::Name { name: s }),
            _ => None,
          },
          query
        }, StatusCode::OK)
        }).into(), StatusCode::OK)
      },
      Err((status, error)) => {
        ResponseCtx(model::Data::Error {error}, status)
        ResponseCtx(model::Root::Error {error}, status)
      }
    }
  }

M src/main.rs => src/main.rs +1 -1
@@ 47,7 47,7 @@ async fn main() {
      let (ctx, dur) = STATE.get_userinfo(&s).await;
      let ResponseCtx(data, status) = ResponseCtx::create(ctx, q.font, q.rest).await;

      let theme = q.theme.filter(|a| STATE.handlebars().has_template(&a)).unwrap_or_else(|| STATE.default_theme());
      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_header(