From d0fc16dde4cf1c58769dfff058509677f601eb8e Mon Sep 17 00:00:00 2001 From: alyx Date: Wed, 9 Aug 2023 15:04:41 -0400 Subject: config done --- .gitignore | 1 + Cargo.toml | 6 ++++ src/README.md | 69 +++++++++++++++++++++++++++++++++++++++++ src/cache.rs | 4 +-- src/config.rs | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 9 ++++-- src/main.rs | 26 +++++++++++++++- themes/test.css | 0 8 files changed, 205 insertions(+), 6 deletions(-) create mode 100644 src/README.md create mode 100644 src/config.rs create mode 100644 themes/test.css diff --git a/.gitignore b/.gitignore index c97d769..b7a23e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/.env /Cargo.lock /target *~ diff --git a/Cargo.toml b/Cargo.toml index 91dcbec..66b6eb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,5 +6,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +dotenv = "0.15.0" +duration-str = "0.5.1" +env_logger = "0.10.0" +log = "0.4.19" +reqwest = { version = "0.11.18", features = ["gzip", "deflate", "brotli", "json"] } serde = { version = "1.0.183", features = ["derive", "rc", "alloc"] } serde_json = "1.0.104" +tokio = { version = "1.29.1", features = ["full"] } diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..e0eb966 --- /dev/null +++ b/src/README.md @@ -0,0 +1,69 @@ +# `lfm_embed` +A simple webserver for rendering a last.fm embed. + +More specifically, this displays a simple webpage with info about your current or most recent last.fm scrobble. +Its intended use is to be put on a personal site or whatever. + +## Usage +`lfm_embed` is, as it stands, designed to use a reverse proxy. +A future release may include HTTPS support, but currently, it will only listen on `localhost`, prohibiting it from public access. +Once configured, a request of the form `http://localhost:9999/` will render out an embed with the default theme. + +As it stands, there are no plans to support displaying users with private listen history. + +## Configuration +Configuration should be done via the environment. +`lfm_embed` also supports reading from a standard `.env` file. +The following are the environment variables which `lfm_embed` understands. + +### `LMFE_API_KEY` (Required) +Your last.fm API key. You'll need to create one [here](https://www.last.fm/api/account/create) for self-hosting. + +### `LFME_WHITELIST_MODE` (Default: open) +The following(case-insensitive) values are supported: +- `open`: Allow requests for all users. +- `exclusive`: Only allow requests for users in `LFME_WHITELIST`, returning HTTP 403 for all others. `LFME_WHITELIST` _must_ be set if this mode is enabled. + +If the user requested has their listen history private, a 403 will be returned. + +### `LFME_WHITELIST` (Default: "") +This is expected to be a sequence of comma-separated usernames. +Leading/trailing whitespace will be stripped, and unicode is fully supported. + +### `LFME_WHITELIST_REFRESH` (Default: "1m") +The amount of time to cache whitelisted user info for. +It is interpreted as a sequence of `` values, where `num` is a positive integer, +and `suffix` is one of `y`,`mon`,`w`,`d`,`h`,`m`,`s`, `ms`, `µs`, or `ns`, each of which +corresponds to a self-explanatory unit of time. +For most practical applications, one should only use `m` and `s`, as caching your current listen for more than that time has the potential to omit most songs. +Parsing is delegated to the [`duration_str`](https://docs.rs/duration-str/latest/duration_str/) crate, and further info may be found there. + +### `LFME_DEFAULT_REFRESH` (Default: "5m") +The amount of time to cache non-whitelisted user info for. +See `LFME_WHITELIST_REFRESH` for more info. + +### `LFME_PORT` (Default: 9999) +The port to serve on locally. + +### `LFME_THEMES_DIR` +If set, must be a valid path to a directory containing CSS files. +They will be registered as themes on top of the builtin ones, with each theme's name being their filename minus the extension. +Same-named themes will override builtin ones. + +### `LFME_LOG_LEVEL` (Default: Warn) +The loglevel. This is actually parsed as an [`env_logger`](https://docs.rs/env_logger/latest/env_logger) filter string. +Read the docs for more info. + +### `LFME_LOG_FILE` +If set, logs will be written to the specified file. Otherwise, logs are written to `stderr`. + +## Example Configuration +```dotenv +LFME_API_KEY=0123456789abcdef0123456789abcdef +LFME_LOG_LEVEL=error +LFME_PORT=3000 + +LFME_WHITELIST_REFRESH=30s +LFME_WHITELIST_MODE=exclusive +LFME_WHITELIST=a_precious_basket_case, realRiversCuomo, Pixiesfan12345 +``` diff --git a/src/cache.rs b/src/cache.rs index 9089ff1..482a9b8 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,5 +1,5 @@ use std::{future::Future, time::*, collections::HashMap, hash::Hash}; - +#[derive(Debug)] pub struct AsyncCache { func: F, cache: HashMap, @@ -12,7 +12,7 @@ where K: Hash + PartialEq + Eq + Clone, Fut: Future> { - pub fn new(interval: Duration, mut func: F) -> Self { + pub fn new(interval: Duration, func: F) -> Self { Self{ cache: HashMap::new(), interval, func diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..ef9c709 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,96 @@ +use std::collections::{HashMap, HashSet}; +use std::error::Error; +use std::sync::LazyLock; +use std::sync::Arc; +use std::future::Future; +use std::pin::Pin; +use std::fs; +use std::time::*; +use duration_str as ds; + +use super::cache::AsyncCache; +use super::deserialize::{GetRecentTracks, GetUserInfo, Track, User}; + +use reqwest::Client; +use dotenv::var; +use tokio::sync::RwLock; + +pub static STATE: LazyLock> = LazyLock::new(|| { + State::new() +}); + +fn getter(username: &String) -> Pin>>> { + Box::pin(async{Err("nope")}) +} + +type Cache = Arc Pin>>>>>>; +#[derive(Debug)] +enum Whitelist { + Exclusive{cache: Cache, whitelist: HashSet}, + Open{default_cache: Cache, whitelist_cache: Cache, whitelist: HashSet} +} +#[derive(Debug)] +pub struct State { + api_key: Arc, + whitelist: Whitelist, + port: u16, + custom_themes: HashMap>, + send_refresh_header: bool +} + +impl State { + fn new() -> Arc { + Arc::new(State { + api_key: var("LFME_API_KEY").expect("API key must be set").into(), + port: var("LFME_PORT").map(|p| p.parse().expect("cannot parse as a port number")).unwrap_or(9999), + send_refresh_header: var("LFME_SET_HEADER").map(|h| &h == "1").unwrap_or(false), + + custom_themes: { + if let Ok(themes_dir) = var("LFME_THEMES_DIR") { + fs::read_dir(themes_dir).expect("error reading LFME_THEMES_DIR") + .map(|a| a.expect("error reading LFME_THEMES_DIR")) + .filter_map(|a| { + let path = a.path(); + if fs::metadata(&path).map(|m| m.is_file()).unwrap_or(false) && + path.extension() == Some("css".as_ref()) { + Some((path.file_stem().unwrap().to_str().expect("bad filename").to_string(), fs::read_to_string(&path).expect("couldn't read theme CSS").into())) + } + else { None } + }) + .collect() + } + else { HashMap::new() } + }, + + whitelist: { + let cache_from_var = |v: &str, d: u64| -> Cache { + let refresh = var(v).map(|r| ds::parse(&r).expect("bad duration string")).unwrap_or_else(|_| Duration::from_secs(d)); + Arc::new(RwLock::new(AsyncCache::new(refresh, getter as _))) + }; + let default_cache = || cache_from_var("LFME_DEFAULT_REFRESH", 300); + let whitelist_cache = || cache_from_var("LFME_WHITELIST_REFRESH", 60); + + let load_whitelist = || -> Option> { + var("LFME_WHITELIST").ok().map( + |w| w.split(",").map(|s| s.trim().to_string()).collect() + ) + }; + + match var("LFME_WHITELIST_MODE").map(|m| m.to_ascii_lowercase()).unwrap_or_else(|_| "open".into()).as_str() { + "open" => { + Whitelist::Open{default_cache: default_cache(), whitelist_cache: whitelist_cache(), whitelist: load_whitelist().unwrap_or_default()} + }, + "exclusive" => { + Whitelist::Exclusive{cache: whitelist_cache(), whitelist: load_whitelist().expect("LFME_WHITELIST not set, unable to serve anyone")} + }, + m => { + panic!("Bad whitelist mode: `{m}`"); + } + } + } + }) + } + + pub fn api_key(&self) -> Arc { self.api_key.clone() } + pub fn port(&self) -> u16 { self.port } +} diff --git a/src/lib.rs b/src/lib.rs index 87a87c5..70522c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,7 @@ -#![feature(entry_insert)] +#![feature(lazy_cell)] -mod deserialize; -mod cache; +pub mod deserialize; +pub mod cache; +pub mod config; + +pub use config::STATE; diff --git a/src/main.rs b/src/main.rs index e7a11a9..13a9b7f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,27 @@ +#![feature(lazy_cell)] + +use dotenv::var; +use log::LevelFilter; +use std::fs::File; +use lfm_embed::STATE; + fn main() { - println!("Hello, world!"); + env_logger::Builder::new() + .filter_level(LevelFilter::Warn) + .parse_filters(&var("LFME_LOG_LEVEL").unwrap_or_default()) + .target( + var("LFME_LOG_FILE").ok() + .map( + |f| env_logger::Target::Pipe( + Box::new(File::options() + .append(true) + .open(f) + .expect("couldn't open LFME_LOG_FILE"))) + ) + .unwrap_or(env_logger::Target::Stderr) + ).init(); + + std::sync::LazyLock::force(&STATE); + + } diff --git a/themes/test.css b/themes/test.css new file mode 100644 index 0000000..e69de29 -- cgit v1.2.3-70-g09d2