aboutsummaryrefslogblamecommitdiffstats
path: root/src/cache/user.rs
blob: 91c496cbd199c0a7776c579d4fd0b73a8e1101cc (plain) (tree)
1
2
3
4
5
6
7
8
                                         





                                  
                                     




                                                         
                                                  




                                                             
                
                              
                                                            















                                                                                                                                                                  
                                             











                                                                                                                                                                                        
                                              
 
                                                                                                                                                                                                                                                   




                                                                                                                                                         
                                                                       

                                                                                                                                                                                                                                    
                                              






                                                    
                                                          
                                                                                                                

                                                                                                                    
               
                                                     

                    

                                                                                      
       
                                                  

          
                                          




                                                                                                                           



                                                                                       




                                                                                   


                                                                             
       
                                                                                               


     



                              
// 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);
}