diff --git a/src/client.rs b/src/client.rs index 0e33ce7c13d4858570ab08b69960585eb16177a7..ef2376a72cd04d0ae5bc7fa92982b82e6637df1c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -120,31 +120,81 @@ fn generate_playlist_api(playlist: &Vec<Song>) -> Vec<ApiSong> { Song::Queued { uuid, url, .. } => ApiSong { uuid: uuid.to_string(), url: url.to_string(), + title: None, + artist: None, + artist_url: None, + length: None, + thumbnail_url: None, state: ApiSongState::Queued, }, - Song::Downloading { uuid, url, .. } => ApiSong { + Song::InfoFetching { uuid, url, .. } => ApiSong { uuid: uuid.to_string(), url: url.to_string(), + title: None, + artist: None, + artist_url: None, + length: None, + thumbnail_url: None, + state: ApiSongState::InfoFetching, + }, + Song::InfoFetched { uuid, url, title, artist, artist_url, length, thumbnail_file, .. } => ApiSong { + uuid: uuid.to_string(), + url: url.to_string(), + title: title.to_owned(), + artist: artist.to_owned(), + artist_url: if let Some(artist_url) = artist_url { Some(artist_url.to_string()) } else { None }, + length: length.to_owned(), + thumbnail_url: Some(thumbnail_file.to_owned()), + state: ApiSongState::InfoFetched, + }, + Song::Downloading { uuid, url, title, artist, artist_url, length, thumbnail_file, .. } => ApiSong { + uuid: uuid.to_string(), + url: url.to_string(), + title: title.to_owned(), + artist: artist.to_owned(), + artist_url: if let Some(artist_url) = artist_url { Some(artist_url.to_string()) } else { None }, + length: length.to_owned(), + thumbnail_url: Some(thumbnail_file.to_owned()), state: ApiSongState::Downloading, }, - Song::Downloaded { uuid, url, .. } => ApiSong { + Song::Downloaded { uuid, url, title, artist, artist_url, length, thumbnail_file, .. } => ApiSong { uuid: uuid.to_string(), url: url.to_string(), + title: title.to_owned(), + artist: artist.to_owned(), + artist_url: if let Some(artist_url) = artist_url { Some(artist_url.to_string()) } else { None }, + length: length.to_owned(), + thumbnail_url: Some(thumbnail_file.to_owned()), state: ApiSongState::Downloaded, }, - Song::Converting { uuid, url, .. } => ApiSong { + Song::Converting { uuid, url, title, artist, artist_url, length, thumbnail_file, .. } => ApiSong { uuid: uuid.to_string(), url: url.to_string(), + title: title.to_owned(), + artist: artist.to_owned(), + artist_url: if let Some(artist_url) = artist_url { Some(artist_url.to_string()) } else { None }, + length: length.to_owned(), + thumbnail_url: Some(thumbnail_file.to_owned()), state: ApiSongState::Converting, }, - Song::Ready { uuid, url, .. } => ApiSong { + Song::Ready { uuid, url, title, artist, artist_url, length, thumbnail_file, .. } => ApiSong { uuid: uuid.to_string(), url: url.to_string(), + title: title.to_owned(), + artist: artist.to_owned(), + artist_url: if let Some(artist_url) = artist_url { Some(artist_url.to_string()) } else { None }, + length: length.to_owned(), + thumbnail_url: Some(thumbnail_file.to_owned()), state: ApiSongState::Ready, }, - Song::Playing { uuid, url, .. } => ApiSong { + Song::Playing { uuid, url, title, artist, artist_url, length, thumbnail_file, .. } => ApiSong { uuid: uuid.to_string(), url: url.to_string(), + title: title.to_owned(), + artist: artist.to_owned(), + artist_url: if let Some(artist_url) = artist_url { Some(artist_url.to_string()) } else { None }, + length: length.to_owned(), + thumbnail_url: Some(thumbnail_file.to_owned()), state: ApiSongState::Playing, }, }) diff --git a/src/downloader.rs b/src/downloader.rs index cc7448c5f46f4ed5be068d27258a9475c7ad1b2f..fe0a1d584ae53560bfcfd81fec7ff590b760e4e7 100644 --- a/src/downloader.rs +++ b/src/downloader.rs @@ -1,5 +1,6 @@ use camino::Utf8Path; use futures_signals::signal::Mutable; +use serde_json::Value; use std::process; use std::thread::{sleep, spawn}; use std::time; @@ -11,12 +12,14 @@ use crate::util; const TMP_DOWNLOAD_DIR: &str = "./tmp"; +const SONGS_INFO_FETCHING_LIMIT: u32 = 5; const SONGS_DOWNLOADING_LIMIT: u32 = 1; const SONGS_CONVERTING_LIMIT: u32 = 1; const SONGS_READY_TARGET: u32 = 3; pub fn manage_downloads(playlist: Mutable<Vec<Song>>) -> () { loop { + let mut songs_info_fetching = 0; let mut songs_downloading = 0; let mut songs_converting = 0; let mut songs_ready = 0; @@ -24,31 +27,71 @@ pub fn manage_downloads(playlist: Mutable<Vec<Song>>) -> () { let mut playlist_guard = playlist.lock_mut(); for song in (*playlist_guard).iter_mut() { - if songs_ready >= SONGS_READY_TARGET { - break; - } - match song { Song::Queued { uuid, url } => { + if songs_info_fetching < SONGS_INFO_FETCHING_LIMIT { + let uuid_clone = uuid.clone(); + let url_clone = url.clone(); + + *song = Song::InfoFetching { + uuid: uuid.clone(), + url: url.clone(), + }; + + let playlist = Mutable::clone(&playlist); + spawn(move || fetch_info(playlist, &uuid_clone, &url_clone)); + + songs_info_fetching += 1; + } + } + Song::InfoFetching { .. } => { + songs_info_fetching += 1; + } + Song::InfoFetched { + uuid, + url, + title, + artist, + artist_url, + length, + thumbnail_file, + } => { if songs_downloading < SONGS_DOWNLOADING_LIMIT - && songs_ready + songs_converting < SONGS_READY_TARGET + && songs_ready + songs_converting + songs_downloading < SONGS_READY_TARGET { - let tmp_file_name = util::random_base64(); - let tmp_file_path = Utf8Path::new(TMP_DOWNLOAD_DIR).join(tmp_file_name); - let uuid = uuid.clone(); let uuid_clone = uuid.clone(); - let url = url.clone(); let url_clone = url.clone(); + let title_clone = title.clone(); + let artist_clone = artist.clone(); + let artist_url_clone = artist_url.clone(); + let length_clone = length.clone(); + let thumbnail_file_clone = thumbnail_file.clone(); + *song = Song::Downloading { - uuid: uuid, - url: url, - audio_file: tmp_file_path.to_string(), + uuid: uuid.clone(), + url: url.clone(), + title: title.clone(), + artist: artist.clone(), + artist_url: artist_url.clone(), + length: length.to_owned(), + thumbnail_file: thumbnail_file.clone(), }; let playlist = Mutable::clone(&playlist); spawn(move || { - download_song(playlist, &uuid_clone, &url_clone, &tmp_file_path) + download_song( + playlist, + &uuid_clone, + &url_clone, + &title_clone, + &artist_clone, + &artist_url_clone, + &length_clone, + &thumbnail_file_clone, + ) }); + + songs_downloading += 1; } } Song::Downloading { .. } => { @@ -57,23 +100,34 @@ pub fn manage_downloads(playlist: Mutable<Vec<Song>>) -> () { Song::Downloaded { uuid, url, + title, + artist, + artist_url, + length, + thumbnail_file, audio_file, } => { - if songs_converting < SONGS_CONVERTING_LIMIT && songs_ready < SONGS_READY_TARGET + if songs_converting < SONGS_CONVERTING_LIMIT + && songs_ready + songs_converting < SONGS_READY_TARGET { - let tmp_file_name = format!("{}.raw", util::random_base64()); - let tmp_file_path = Utf8Path::new(TMP_DOWNLOAD_DIR).join(tmp_file_name); - let uuid = uuid.clone(); let uuid_clone = uuid.clone(); - let url = url.clone(); let url_clone = url.clone(); - let audio_file = audio_file.clone(); + let title_clone = title.clone(); + let artist_clone = artist.clone(); + let artist_url_clone = artist_url.clone(); + let length_clone = length.clone(); + let thumbnail_file_clone = thumbnail_file.clone(); let audio_file_clone = audio_file.clone(); + *song = Song::Converting { uuid: uuid.clone(), - url: url, - audio_file: audio_file, - raw_file: tmp_file_path.to_string(), + url: url.clone(), + title: title.clone(), + artist: artist.clone(), + artist_url: artist_url.clone(), + length: length.to_owned(), + thumbnail_file: thumbnail_file.clone(), + audio_file: audio_file.clone(), }; let playlist = Mutable::clone(&playlist); @@ -82,12 +136,17 @@ pub fn manage_downloads(playlist: Mutable<Vec<Song>>) -> () { playlist, &uuid_clone, &url_clone, + &title_clone, + &artist_clone, + &artist_url_clone, + &length_clone, + &thumbnail_file_clone, &audio_file_clone, - &tmp_file_path, ) }); + + songs_converting += 1; } - songs_converting += 1; } Song::Converting { .. } => { songs_converting += 1; @@ -105,24 +164,114 @@ pub fn manage_downloads(playlist: Mutable<Vec<Song>>) -> () { } } -fn download_song(playlist: Mutable<Vec<Song>>, uuid: &Uuid, url: &Url, audio_file: &Utf8Path) { - println!("Downloading from {} to {}!", url, audio_file); +fn fetch_info(playlist: Mutable<Vec<Song>>, uuid: &Uuid, url: &Url) { + println!("Fetching info for {}!", url); + + let tmp_file_name = util::random_base64(); + let tmp_file_path = Utf8Path::new(TMP_DOWNLOAD_DIR).join(tmp_file_name); + + process::Command::new("yt-dlp") + .args([ + "--output", + tmp_file_path.as_str(), + "--write-info-json", + "--write-thumbnail", + "--skip-download", + url.as_str(), + ]) + .output() + .expect("Failed to fetch info!"); + let info_json_file = format!("{}.info.json", tmp_file_path); + + let data = std::fs::read_to_string(info_json_file).expect("Could not find info.json!"); + + let info_json: Value = serde_json::from_str(&data).expect("Could not parse info.json!"); + + let mut title = None; + if let Value::String(string) = &info_json["title"] { + title = Some(string.to_owned()); + } else { + eprintln!("Could not fetch title!"); + } + + let mut artist = None; + if let Value::String(string) = &info_json["channel"] { + artist = Some(string.to_owned()); + } else { + eprintln!("Could not fetch artist!"); + } + + let mut artist_url = None; + if let Value::String(string) = &info_json["channel_url"] { + if let Ok(url) = Url::parse(&string) { + artist_url = Some(url); + } else { + eprintln!("Invalid artist_url!"); + } + } else { + eprintln!("Could not fetch artist_url!"); + } + + let mut length = None; + if let Value::Number(number) = &info_json["duration"] { + length = number.as_u64(); + } else { + eprintln!("Could not fetch artist_url!"); + } + println!("Fetched all info!"); + + let thumbnail_file = format!("{}.webm", tmp_file_path); + + let mut playlist = playlist.lock_mut(); + if let Some(index) = (*playlist).iter().position(|song| song.equals_uuid(uuid)) { + playlist[index] = Song::InfoFetched { + uuid: uuid.to_owned(), + url: url.to_owned(), + title: title, + artist: artist, + artist_url: artist_url, + length: length, + thumbnail_file: thumbnail_file.to_owned(), + }; + } + drop(playlist); +} + +fn download_song( + playlist: Mutable<Vec<Song>>, + uuid: &Uuid, + url: &Url, + title: &Option<String>, + artist: &Option<String>, + artist_url: &Option<Url>, + length: &Option<u64>, + thumbnail_file: &str, +) { + let audio_file_name = util::random_base64(); + let audio_file_path = Utf8Path::new(TMP_DOWNLOAD_DIR).join(audio_file_name); + + println!("Downloading from {} to {}!", url, audio_file_path); process::Command::new("yt-dlp") - .args(["--output", audio_file.as_str(), url.as_str()]) + .args(["--output", audio_file_path.as_str(), url.as_str()]) .output() .expect("Failed to download audio!"); println!("Download successful!"); - let audio_file = format!("{}.webm", audio_file); + let audio_file_path = format!("{}.webm", audio_file_path); let mut playlist = playlist.lock_mut(); if let Some(index) = (*playlist).iter().position(|song| song.equals_uuid(uuid)) { playlist[index] = Song::Downloaded { uuid: uuid.to_owned(), url: url.to_owned(), - audio_file: audio_file.to_owned(), + title: title.to_owned(), + artist: artist.to_owned(), + artist_url: artist_url.to_owned(), + length: length.to_owned(), + thumbnail_file: thumbnail_file.to_owned(), + audio_file: audio_file_path.to_owned(), }; } drop(playlist); @@ -132,12 +281,19 @@ fn convert_song( playlist: Mutable<Vec<Song>>, uuid: &Uuid, url: &Url, + title: &Option<String>, + artist: &Option<String>, + artist_url: &Option<Url>, + length: &Option<u64>, + thumbnail_file: &str, audio_file: &str, - raw_file: &Utf8Path, ) { let audio_file = Utf8Path::new(audio_file); - println!("Converting from {} to {}!", audio_file, raw_file); + let raw_file_name = format!("{}.raw", util::random_base64()); + let raw_file_path = Utf8Path::new(TMP_DOWNLOAD_DIR).join(raw_file_name); + + println!("Converting from {} to {}!", audio_file, raw_file_path); process::Command::new("ffmpeg") .args([ @@ -151,7 +307,7 @@ fn convert_song( "384k", "-ac", "1", - raw_file.as_str(), + raw_file_path.as_str(), ]) .output() .expect("Failed to convert audio!"); @@ -164,7 +320,12 @@ fn convert_song( playlist[index] = Song::Ready { uuid: uuid.to_owned(), url: url.to_owned(), - raw_file: raw_file.as_str().to_owned(), + title: title.to_owned(), + artist: artist.to_owned(), + artist_url: artist_url.to_owned(), + length: length.to_owned(), + thumbnail_file: thumbnail_file.to_owned(), + raw_file: raw_file_path.as_str().to_owned(), }; } drop(playlist); diff --git a/src/main.rs b/src/main.rs index 910662c32f93f6aef9ec21d472198cb3757f251b..0f5ede1d46e2d91caf832be29145e97f6290d832 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,12 +33,19 @@ fn main() { .expect("No client token provided!"), ); - let bind_host = - env::var("SPOCCIFY_BIND_HOST").unwrap_or("localhost".to_owned()); - let bind_port = - env::var("SPOCCIFY_BIND_PORT").unwrap_or("8080".to_owned()); + let http_bind_host = + env::var("SPOCCIFY_HTTP_BIND_HOST").unwrap_or("localhost".to_owned()); + let http_bind_port = + env::var("SPOCCIFY_HTTP_BIND_PORT").unwrap_or("9000".to_owned()); - let bind_address = format!("{}:{}", bind_host, bind_port); + let http_bind_address = format!("{}:{}", http_bind_host, http_bind_port); + + let websocket_bind_host = + env::var("SPOCCIFY_WEBSOCKET_BIND_HOST").unwrap_or("localhost".to_owned()); + let websocket_bind_port = + env::var("SPOCCIFY_WEBSOCKET_BIND_PORT").unwrap_or("9001".to_owned()); + + let websocket_bind_address = format!("{}:{}", websocket_bind_host, websocket_bind_port); println!("Listening for websocket connections on {}!", bind_address); diff --git a/src/player.rs b/src/player.rs index ea3439874055b508da409cb2c2ab18143f679321..428d4126f02f4fa62d7dd9ced80375d47b8915e1 100644 --- a/src/player.rs +++ b/src/player.rs @@ -86,6 +86,11 @@ fn get_next_song_file(playlist: &Mutable<Vec<Song>>) -> std::io::Result<File> { if let Song::Ready { uuid, url, + title, + artist, + artist_url, + length, + thumbnail_file, raw_file, } = &playlist[0] { @@ -96,6 +101,11 @@ fn get_next_song_file(playlist: &Mutable<Vec<Song>>) -> std::io::Result<File> { playlist[0] = Song::Playing { uuid: uuid.clone(), url: url.clone(), + title: title.clone(), + artist: artist.clone(), + artist_url: artist_url.clone(), + length: length.to_owned(), + thumbnail_file: thumbnail_file.clone(), raw_file: raw_file, }; file = File::open(raw_file_clone); diff --git a/src/song.rs b/src/song.rs index 968fa938a7f2e2adbfd9a2432ff268eee32345c6..76fd2a5f816fa0364a654be8539f3fc1276fe291 100644 --- a/src/song.rs +++ b/src/song.rs @@ -8,30 +8,66 @@ pub enum Song { uuid: Uuid, url: Url, }, + InfoFetching { + uuid: Uuid, + url: Url, + }, + InfoFetched { + uuid: Uuid, + url: Url, + title: Option<String>, + artist: Option<String>, + artist_url: Option<Url>, + length: Option<u64>, + thumbnail_file: String, + }, Downloading { uuid: Uuid, url: Url, - audio_file: String, + title: Option<String>, + artist: Option<String>, + artist_url: Option<Url>, + length: Option<u64>, + thumbnail_file: String, }, Downloaded { uuid: Uuid, url: Url, + title: Option<String>, + artist: Option<String>, + artist_url: Option<Url>, + length: Option<u64>, + thumbnail_file: String, audio_file: String, }, Converting { uuid: Uuid, url: Url, + title: Option<String>, + artist: Option<String>, + artist_url: Option<Url>, + length: Option<u64>, + thumbnail_file: String, audio_file: String, - raw_file: String, }, Ready { uuid: Uuid, url: Url, + title: Option<String>, + artist: Option<String>, + artist_url: Option<Url>, + length: Option<u64>, + thumbnail_file: String, raw_file: String, }, Playing { uuid: Uuid, url: Url, + title: Option<String>, + artist: Option<String>, + artist_url: Option<Url>, + length: Option<u64>, + thumbnail_file: String, raw_file: String, }, } @@ -40,6 +76,8 @@ impl Song { pub fn equals_uuid(&self, uuid_compare: &Uuid) -> bool { match self { Self::Queued { uuid, .. } => uuid == uuid_compare, + Self::InfoFetching { uuid, .. } => uuid == uuid_compare, + Self::InfoFetched { uuid, .. } => uuid == uuid_compare, Self::Downloading { uuid, .. } => uuid == uuid_compare, Self::Downloaded { uuid, .. } => uuid == uuid_compare, Self::Converting { uuid, .. } => uuid == uuid_compare, @@ -54,11 +92,18 @@ pub struct ApiSong { pub uuid: String, pub url: String, pub state: ApiSongState, + pub title: Option<String>, + pub artist: Option<String>, + pub artist_url: Option<String>, + pub length: Option<u64>, + pub thumbnail_url: Option<String>, } #[derive(Debug, Serialize, Deserialize)] pub enum ApiSongState { Queued, + InfoFetching, + InfoFetched, Downloading, Downloaded, Converting,