~aleteoryx/lfm_embed

ref: 7d8d5fa6f9bfafe07932728fe86ba14713377b7b lfm_embed/src/config.rs -rw-r--r-- 7.9 KiB
7d8d5fa6alyx Fix README typos 1 year, 3 months ago
                                                                                
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
use std::collections::{HashMap, HashSet};
use std::sync::LazyLock;
use std::sync::Arc;
use std::future::Future;
use std::pin::Pin;
use std::time::*;

use super::cache::AsyncCache;
use super::deserialize::{GetRecentTracks, GetUserInfo, Track, User};

use reqwest::{Client, StatusCode};
use dotenv::var;
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"))];

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 = 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
            .map_err(|e| {log::error!("Failed to get info for user `{username}`: {e}"); (StatusCode::SERVICE_UNAVAILABLE, "Couldn't connect to last.fm!")})?;
        if userreq.status() == StatusCode::NOT_FOUND { return Err((StatusCode::NOT_FOUND, "User does not exist!")); }

        let userinfo = userreq.json::<GetUserInfo>().await
            .map_err(|e| {log::error!("Couldn't parse user.getInfo for `{username}`: {e}"); (StatusCode::INTERNAL_SERVER_ERROR, "Couldn't parse user.getInfo!")})?.user;
        
        let tracksreq = STATE.http.get(format!("https://ws.audioscrobbler.com/2.0/?method=user.getRecentTracks&format=json&extended=1&limit=1&user={username}&api_key={}", STATE.api_key))
            .send().await
            .map_err(|e| {log::error!("Failed to get tracks for user `{username}`: {e}"); (StatusCode::SERVICE_UNAVAILABLE, "Couldn't connect to last.fm!")})?;
        if tracksreq.status() == StatusCode::NOT_FOUND { return Err((StatusCode::NOT_FOUND, "User does not exist!")); }
        if tracksreq.status() == StatusCode::FORBIDDEN { return Err((StatusCode::FORBIDDEN, "You need to unprivate your song history!")); }

        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!"))?;

        Ok(Arc::new((userinfo, tracksinfo)))
    })
}
type Getter = fn(&String) -> Pin<Box<(dyn Future<Output = Result<Arc<(User, Track)>, (StatusCode, &'static str)>> + Send + Sync)>>;
type Cache = Arc<RwLock<AsyncCache<String, Arc<(User, Track)>, Getter>>>;
#[derive(Debug)]
enum Whitelist {
    Exclusive{cache: Cache, whitelist: HashSet<String>},
    Open{default_cache: Cache, whitelist_cache: Cache, whitelist: HashSet<String>}
}
#[derive(Debug)]
pub struct State {
    api_key: Arc<str>,
    whitelist: Whitelist,
    port: u16,
    handlebars: Handlebars<'static>,
    default_theme: Arc<str>,
    send_refresh_header: bool,
    http: Client,

    default_refresh: Duration,
    whitelist_refresh: Duration
}

impl State {
    fn new() -> Arc<Self> {
        let duration_from_var = |v: &str, d: u64| -> Duration {var(v).map(|r| ds::parse(&r).expect("bad duration string")).unwrap_or_else(|_| Duration::from_secs(d))};
        let cache_from_duration = |d: Duration| -> Cache {
            Arc::new(RwLock::new(AsyncCache::new(d, getter as Getter)))
        };
        let default_refresh = duration_from_var("LFME_DEFAULT_REFRESH", 300);
        let whitelist_refresh = duration_from_var("LFME_WHITELIST_REFRESH", 60);
        let default_cache = cache_from_duration(default_refresh);
        let whitelist_cache = cache_from_duration(whitelist_refresh);

        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_NO_REFRESH").map(|h| &h == "1").unwrap_or(false),
            http: Client::builder().https_only(true).user_agent(concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"))).build().unwrap(),
            
            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();
                }
                hb.set_dev_mode(var("LFME_THEME_DEV").map(|h| &h == "1").unwrap_or(false));
                
                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
            },
            default_theme: var("LFME_THEME_DEFAULT").map(Into::into).unwrap_or_else(|_| "plain".into()),
            
            whitelist: {
                let load_whitelist = || -> Option<HashSet<String>> {
                    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, 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}`");
                    }
                }
            },
            default_refresh: default_refresh + Duration::from_secs(1),
            whitelist_refresh: whitelist_refresh + Duration::from_secs(1)
        })
    }
    
    pub fn port(&self) -> u16 { self.port }
    pub fn send_refresh_header(&self) -> bool { self.send_refresh_header }
    pub async fn get_userinfo(&self, user: &String) -> (Result<Arc<(User, Track)>, (StatusCode, &'static str)>, Duration) {
        match &self.whitelist {
            Whitelist::Open{default_cache, whitelist_cache, whitelist} => {
                if whitelist.contains(user) {
                    (whitelist_cache.write().await.get_owned(user).await, self.whitelist_refresh)
                }
                else {
                    (default_cache.write().await.get_owned(user).await, self.default_refresh)
                }
            },
            Whitelist::Exclusive{cache, whitelist} => {
                if whitelist.contains(user) {
                    (cache.write().await.get_owned(user).await, self.whitelist_refresh)
                }
                else { (Err((StatusCode::FORBIDDEN, "User not in whitelist!")), self.default_refresh) }
            }
        }
    }
    pub fn handlebars(&self) -> &Handlebars { &self.handlebars }
    pub fn default_theme(&self) -> Arc<str> { self.default_theme.clone() }
}