use std::{fmt::Display, fs, ops::Deref, sync::OnceLock};
use lunar_lib::prompts::{AskPrompter, CliPrompter};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{REQWEST_CLIENT, data_dir};
mod error;
pub use error::*;
pub const LASTFM_API_KEY: &str = "aeffe4a6d9c3b1d996c8442f473d4de4";
pub const LASTFM_API_SECRET: &str = "7953741c2b6857dddf5833de7e7d14b0";
const LASTFM_SCROBBLER_ORIGIN: &str = "http://ws.audioscrobbler.com/2.0";
const LASTFM_ORIGIN: &str = "http://www.last.fm/api";
static LASTFM_CLIENT: OnceLock<LastFmClient> = OnceLock::new();
pub async fn initialize_last_fm_client() -> bool {
if LASTFM_CLIENT.get().is_some() {
return true;
}
if let Some(flatten) = LastFmClient::load().await.ok().flatten() {
let _ = LASTFM_CLIENT.set(flatten);
return true;
}
false
}
pub fn last_fm_client() -> Option<&'static LastFmClient> {
LASTFM_CLIENT.get()
}
mod api_methods;
pub use api_methods::*;
#[derive(Serialize, Deserialize)]
pub struct LastFmClient {
name: String,
key: String,
subscriber: bool,
}
#[derive(Debug, Error)]
pub enum LastFmClientError {
#[error("{0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
LastFmError(#[from] LastFmError),
#[error("{0}")]
LastFmMethodError(#[from] LastFmMethodError),
#[error("{0}")]
Keyring(#[from] keyring::Error),
}
impl LastFmClient {
async fn request_from_token(token: &LastFmToken) -> Result<LastFmClient, LastFmError> {
let mut query = vec![
("method", "auth.getsession"),
("api_key", LASTFM_API_KEY),
("token", token),
];
let sig = request_sig(&query);
query.push(("api_sig", &*sig));
query.push(("format", "json"));
#[derive(Deserialize)]
struct Inner {
name: String,
key: String,
subscriber: isize,
}
#[derive(Deserialize)]
struct Wrapper {
session: Inner,
}
let wrapper = REQWEST_CLIENT
.get(LASTFM_SCROBBLER_ORIGIN)
.query(&query)
.send()
.await
.unwrap()
.json::<LastFmResult<Wrapper>>()
.await
.unwrap()
.into_result()?;
Ok(Self {
name: wrapper.session.name,
key: wrapper.session.key,
subscriber: wrapper.session.subscriber > 0,
})
}
pub async fn connect<F>(on_wait: F) -> Result<LastFmClient, LastFmError>
where
F: FnOnce(),
{
let token = get_token().await?;
open_auth_page(&token);
on_wait();
Self::request_from_token(&token).await
}
pub async fn save(&self) -> Result<(), LastFmClientError> {
let json = serde_json::to_string(self).expect("Serialization should not fail");
let keyring_json = json.clone();
let result = tokio::task::spawn_blocking(move || -> Result<(), keyring::Error> {
keyring::Entry::new("selene", "lastfm")?.set_password(&keyring_json)
})
.await
.unwrap();
if let Err(err) = result {
let prompter = CliPrompter;
let yes = prompter.ask(format_args!("The session key could not be saved in the systems keyring (Error: {err}). Would you like to save it as a plaintext file in Selene's data folder?'"), false).unwrap_or(false);
if yes {
fs::write(data_dir().join("lastfm_session.json"), &json)?;
}
}
Ok(())
}
pub async fn load() -> Result<Option<LastFmClient>, LastFmClientError> {
let result = tokio::task::spawn_blocking(|| -> Result<String, keyring::Error> {
keyring::Entry::new("selene", "lastfm")?.get_password()
})
.await
.unwrap();
match result {
Ok(json) => {
let client: LastFmClient =
serde_json::from_str(&json).expect("LastFM Client data was modified");
Ok(Some(client))
}
Err(keyring::Error::NoEntry) => {
let key_file = data_dir().join("lastfm_session.json");
if !key_file.exists() {
return Ok(None);
}
let json = fs::read_to_string(&key_file)?;
let client: LastFmClient =
serde_json::from_str(&json).expect("LastFM Client data was modified");
Ok(Some(client))
}
Err(err) => Err(err.into()),
}
}
}
fn request_sig<K, V>(params: &[(K, V)]) -> String
where
K: AsRef<str>,
V: AsRef<str>,
{
let mut params: Vec<(&str, &str)> = params
.iter()
.map(|(k, v)| (k.as_ref(), v.as_ref()))
.collect();
params.sort_by_key(|(k, _)| *k);
let mut seed: String = params.iter().map(|(k, v)| format!("{k}{v}")).collect();
seed.push_str(LASTFM_API_SECRET);
format!("{:x}", md5::compute(seed))
}
#[derive(Deserialize)]
pub struct LastFmToken {
token: String,
}
impl Display for LastFmToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.token.fmt(f)
}
}
impl Deref for LastFmToken {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.token
}
}
async fn get_token() -> Result<LastFmToken, LastFmError> {
let query = [
("method", "auth.gettoken"),
("api_key", LASTFM_API_KEY),
("format", "json"),
];
REQWEST_CLIENT
.get(LASTFM_SCROBBLER_ORIGIN)
.query(&query)
.send()
.await
.unwrap()
.json::<LastFmResult<LastFmToken>>()
.await
.unwrap()
.into_result()
}
fn open_auth_page(token: &LastFmToken) {
let page = format!("{LASTFM_ORIGIN}/auth/?api_key={LASTFM_API_KEY}&token={token}");
open::that_detached(page).expect("Failed to open webpage");
}