use std::time::Duration;
use futures::{StreamExt, stream};
use reqwest::Url;
use serde_json::Value;
use crate::{command, info_log, lyrics::Lyrics};
const BASE_URL: &str = "https://lrclib.net/api/";
const MAX_DEVIATION: f64 = 1.;
const MAX_CONCURRENCE: usize = 4;
const MAX_TITLE_LENGTH: usize = 25;
const RESPONSE_TIMEOUT: f64 = 5.0;
#[derive(Clone, Debug)]
pub struct Song {
pub data: SongData,
pub lyrics: Option<Lyrics>,
}
impl Song {
pub async fn request_song(data: SongData) -> Song {
info_log(format!("Requesting {}", data.get_title_truncated(MAX_TITLE_LENGTH)));
let mut stream = Box::pin(stream::iter((0..=3).into_iter())
.map(|i| data.request_lyrics(i))
.buffer_unordered(MAX_CONCURRENCE)
.filter_map(|choices| async move {
let choices = choices?;
let Some(duration) = data.duration else { return Some(choices) };
let choices: Vec<Lyrics> = choices
.into_iter()
.filter(|l| (l.duration - duration).abs() < MAX_DEVIATION)
.collect();
Some(choices)
})
.filter(|v| {
let empty = v.is_empty();
async move { !empty }
}));
let mut choices: Vec<Lyrics> = vec![];
let mut timeout = Box::pin(tokio::time::sleep(Duration::from_secs_f64(RESPONSE_TIMEOUT)));
loop {
tokio::select! {
n = stream.next() => {
let Some(mut n) = n else {
break;
};
choices.append(&mut n);
},
_ = (&mut timeout) => if !choices.is_empty() {
info_log("Timed out, only some lyrics found");
break;
}
}
}
drop(stream);
if choices.is_empty() {
info_log(format!("Couldn't retrieve lyrics for {}", data.get_title_truncated(MAX_TITLE_LENGTH)));
return Song {
data,
lyrics: None,
}
}
info_log(format!("{} Lyrics Found", choices.len()));
if let Some(duration) = data.duration {
choices.sort_by(|a, b| (a.duration - duration).abs().total_cmp(&(b.duration - duration).abs()));
}
let lyrics = choices.into_iter().nth(0);
Song {
data,
lyrics,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct SongData {
title: String,
artist: Option<String>,
album: Option<String>,
duration: Option<f64>,
pub player: Option<Player>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Player {
Spotify
}
impl ToString for Player {
fn to_string(&self) -> String {
match self {
Self::Spotify => "spotify"
}.to_string()
}
}
pub fn get_flag_from_player(player: &Option<Player>) -> String {
if let Some(player) = &player {
format!("-p {}", player.to_string())
} else { "".to_string() }
}
impl SongData {
pub fn get_data() -> Option<Self> {
let spotify_data = SongData::get_data_from_player(Some(Player::Spotify));
spotify_data.or(SongData::get_data_from_player(None))
}
fn get_title_truncated(&self, max_length: usize) -> String {
if self.title.len() <= max_length {
self.title.clone()
} else {
format!("{}...", self.title.chars().take(max_length).collect::<String>())
}
}
fn get_data_from_player(player: Option<Player>) -> Option<Self> {
let flag = get_flag_from_player(&player);
let metadata_command = format!("playerctl {flag} metadata ");
let get_attr = |name: &str|
Some(
command(&(metadata_command.clone() + name)).trim().to_string(),
).filter(|s| !s.is_empty());
let title = get_attr("title")?;
let artist = get_attr("artist");
let album = get_attr("album");
let duration = get_attr("mpris:length")
.map(|d| d.parse::<f64>())
.and_then(|result| result.ok())
.map(|d| d / 1e6);
Some(Self {
title,
artist,
album,
duration,
player,
})
}
async fn request_lyrics(&self, precision: u8) -> Option<Vec<Lyrics>> {
let request = self.format_request(precision);
let res = reqwest::get(request).await.ok()?;
let body = res.text().await.ok()?;
let json: Value = serde_json::from_str(&body).ok()?;
if json.is_array() {
let lyrics: Vec<Lyrics> = json.as_array()?
.iter()
.filter_map(|json| Lyrics::from_json(json))
.collect();
Some(lyrics)
} else {
Lyrics::from_json(&json).map(|l| vec![l])
}
}
fn format_request(&self, precision: u8) -> Url {
let precise =
precision == 0 &&
self.artist.is_some() &&
self.album.is_some() &&
self.duration.is_some();
let mut attributes =
self.artist.is_some() as u8 +
self.album.is_some() as u8 +
self.duration.is_some() as u8;
attributes -= precision.min(attributes);
if !precise && attributes == 3 { attributes -= 1}
let mut url = Url::parse(BASE_URL).expect("Invalid url??");
url.set_path(if precise {
"api/get"
} else {
"api/search"
});
url.query_pairs_mut()
.append_pair("track_name", &self.title);
let add_pair = |precision: &mut u8, url: &mut Url, key: &str, value: &str| {
if *precision <= 0 { return }
*precision -= 1;
url.query_pairs_mut().append_pair(key, value);
};
if let Some(artist) = &self.artist {
add_pair(&mut attributes, &mut url, "artist_name", artist);
}
if let Some(album) = &self.album {
add_pair(&mut attributes, &mut url, "album_name", album);
}
if let Some(duration) = &self.duration {
add_pair(&mut attributes, &mut url, "duration", &duration.to_string());
}
url
}
}