aboutsummaryrefslogtreecommitdiffstats
path: root/src/cache/user.rs
blob: 91c496cbd199c0a7776c579d4fd0b73a8e1101cc (plain) (blame)
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
// SPDX-License-Identifier: AGPL-3.0-only
use std::sync::Arc;
use std::time::Duration;
use std::sync::LazyLock;

use tokio::sync::RwLock;
use reqwest::{StatusCode, Client};
use urlencoding::encode as urlencode;

use super::{CacheFuture, CacheGetter, Cache, AsyncCache};
use crate::deserialize::{User, Track, TrackStub};
use crate::CONFIG;

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

#[derive(Debug)]
enum Allowlist {
  Exclusive{cache: UserCache},
  Open{default_cache: UserCache, allowlist_cache: UserCache}
}

fn user_getter(username: &String) -> UserFuture {
  use crate::deserialize::{GetUserInfo, GetRecentTracks, GetTrackInfo};

  let username = urlencoding::encode(username.as_ref()).to_string();
  Box::pin(async move {
    let userreq = HTTP.get(format!("https://ws.audioscrobbler.com/2.0/?method=user.getInfo&format=json&user={username}&api_key={}", CONFIG.lastfm_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 userstr = userreq.text().await.unwrap();
    log::trace!("Got user.getUserInfo JSON for `{username}`: {userstr}");
    let userinfo = serde_json::from_str::<GetUserInfo>(&userstr)
      .map_err(|e| {log::error!("Couldn't parse user.getInfo for `{username}`: {e}"); (StatusCode::INTERNAL_SERVER_ERROR, "Couldn't parse user.getInfo!")})?.user;
    log::trace!("Parsed into: {userinfo:?}");

    let tracksreq = HTTP.get(format!("https://ws.audioscrobbler.com/2.0/?method=user.getRecentTracks&format=json&extended=1&limit=1&user={username}&api_key={}", CONFIG.lastfm_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 tracksstr = tracksreq.text().await.unwrap();
    log::trace!("Got user.getRecentTracks JSON for `{username}`: {tracksstr}");
    let trackstub = serde_json::from_str::<GetRecentTracks>(&tracksstr)
      .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().next().ok_or((StatusCode::UNPROCESSABLE_ENTITY, "You need to listen to some songs first!"))?;
    log::trace!("Parsed into: {trackstub:?}");

    let trackreq = HTTP.get(format!("https://ws.audioscrobbler.com/2.0/?method=track.getInfo&format=json&username={username}&api_key={}&track={}&artist={}", CONFIG.lastfm_api_key, urlencode(&trackstub.name), urlencode(&trackstub.artist.name)))
      .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 trackreq.status() == StatusCode::NOT_FOUND { return Err((StatusCode::NOT_FOUND, "Track does not exist!")); }

    let trackstr = trackreq.text().await.unwrap();
    log::trace!("Got track.getInfo JSON for `{username}`: {trackstr}");
    let trackinfo = serde_json::from_str::<GetTrackInfo>(&trackstr)
      .map_err(|e| {log::error!("Couldn't parse track.getInfo for `{}` by `{}` on behalf of {username}: {e}", trackstub.name, trackstub.artist.name); (StatusCode::INTERNAL_SERVER_ERROR, "Couldn't parse track.getInfo!")})?.track;
    log::trace!("Parsed into: {trackinfo:?}");

    Ok(Arc::new((userinfo, trackinfo, trackstub)))
  })
}

static HTTP: LazyLock<Client> = crate::http::lazy();

static ALLOWLIST: LazyLock<Allowlist> = LazyLock::new(|| {
  let default_cache = Arc::new(RwLock::new(AsyncCache::new(CONFIG.default_refresh, user_getter as UserGetter)));
  let allowlist_cache = Arc::new(RwLock::new(AsyncCache::new(CONFIG.allowlist_refresh, user_getter as UserGetter)));
  match CONFIG.allowlist_mode.as_str() {
    "open" => {
      Allowlist::Open{default_cache, allowlist_cache}
    },
    "exclusive" => {
      if CONFIG.allowlist.is_empty() {
        panic!("Exclusive mode set with empty allowlist, cannot serve any requests!");
      }
      Allowlist::Exclusive{cache: allowlist_cache}
    },
    m => {
      panic!("Bad allowlist mode: `{m}`");
    }
  }
});

pub async fn get_userinfo(user: &String) -> (Result<Arc<(User, Track, TrackStub)>, (StatusCode, &'static str)>, Duration) {
  match LazyLock::force(&ALLOWLIST) {
    Allowlist::Open{default_cache, allowlist_cache} => {
      if CONFIG.allowlist.contains(user) {
        (allowlist_cache.write().await.get_owned(user).await, CONFIG.allowlist_refresh)
      }
      else {
        (default_cache.write().await.get_owned(user).await, CONFIG.default_refresh)
      }
    },
    Allowlist::Exclusive{cache} => {
      if CONFIG.allowlist.contains(user) {
        (cache.write().await.get_owned(user).await, CONFIG.allowlist_refresh)
      }
      else { (Err((StatusCode::FORBIDDEN, "User not in allowlist!")), CONFIG.default_refresh) }
    }
  }
}

pub fn touch() {
  LazyLock::force(&ALLOWLIST);
}