use std::time::Duration;
use futures::{StreamExt, stream};
use reqwest::Url;
use serde_json::Value;
use tokio::time::timeout;
use crate::{command, info_log, lyrics::Lyrics};
const BASE_URL: &str = "https://lrclib.net/api/";
const MAX_DEVIATION: f64 = 5.;
const MAX_CONCURRENCE: usize = 4;
const MAX_TITLE_LENGTH: usize = 30;
const RESPONSE_TIMEOUT: f64 = 5.0;
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 choices: Vec<Lyrics> = 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 }
})
.flat_map(stream::iter)
.collect().await;
if choices.is_empty() {
info_log(format!("Coudln't retrieve lyrics for {}", data.get_title_truncated(MAX_TITLE_LENGTH)));
return Song {
data,
lyrics: None,
}
}
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);
info_log("Lyrics successfully found");
Song {
data,
lyrics,
}
}
}
#[derive(Debug, PartialEq)]
pub struct SongData {
title: String,
artist: Option<String>,
album: Option<String>,
duration: Option<f64>,
}
impl SongData {
pub fn get_data() -> Option<Self> {
let spotify_data = SongData::get_data_from_player("spotify");
spotify_data.or(SongData::get_data_from_player(""))
}
fn get_title_truncated(&self, max_length: usize) -> String {
if self.title.len() <= max_length - 3 {
self.title.clone()
} else {
format!("{}...", self.title[..max_length-3].to_string())
}
}
fn get_data_from_player(player: &str) -> Option<Self> {
let flag = format!(" -p {player}");
let playerctl = "playerctl".to_string() +
if player.is_empty() { "" } else { &flag } +
" metadata ";
let get_attr = |name: &str|
Some(
command(&(playerctl.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,
})
}
async fn request_lyrics(&self, precision: u8) -> Option<Vec<Lyrics>> {
let request = self.format_request(precision);
let res = timeout(
Duration::from_secs_f64(RESPONSE_TIMEOUT),
reqwest::get(request),
).await.ok().or_else(|| {
info_log(" Timed Out");
None
})?.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;
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
}
}