diff --git a/Cargo.lock b/Cargo.lock index ae8ffae..44f0153 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2275,6 +2275,7 @@ dependencies = [ "sled", "symphonia", "tokio", + "ureq", "uuid", "walkdir", ] @@ -2646,8 +2647,6 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "symphonia" version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" dependencies = [ "lazy_static", "symphonia-bundle-flac", @@ -2669,8 +2668,6 @@ dependencies = [ [[package]] name = "symphonia-bundle-flac" version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97" dependencies = [ "log", "symphonia-core", @@ -2681,8 +2678,6 @@ dependencies = [ [[package]] name = "symphonia-bundle-mp3" version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" dependencies = [ "lazy_static", "log", @@ -2693,8 +2688,6 @@ dependencies = [ [[package]] name = "symphonia-codec-aac" version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdbf25b545ad0d3ee3e891ea643ad115aff4ca92f6aec472086b957a58522f70" dependencies = [ "lazy_static", "log", @@ -2704,8 +2697,6 @@ dependencies = [ [[package]] name = "symphonia-codec-adpcm" version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c94e1feac3327cd616e973d5be69ad36b3945f16b06f19c6773fc3ac0b426a0f" dependencies = [ "log", "symphonia-core", @@ -2714,8 +2705,6 @@ dependencies = [ [[package]] name = "symphonia-codec-alac" version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d8a6666649a08412906476a8b0efd9b9733e241180189e9f92b09c08d0e38f3" dependencies = [ "log", "symphonia-core", @@ -2724,8 +2713,6 @@ dependencies = [ [[package]] name = "symphonia-codec-pcm" version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b" dependencies = [ "log", "symphonia-core", @@ -2734,8 +2721,6 @@ dependencies = [ [[package]] name = "symphonia-codec-vorbis" version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30" dependencies = [ "log", "symphonia-core", @@ -2745,8 +2730,6 @@ dependencies = [ [[package]] name = "symphonia-core" version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" dependencies = [ "arrayvec", "bitflags 1.3.2", @@ -2758,8 +2741,6 @@ dependencies = [ [[package]] name = "symphonia-format-caf" version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e43c99c696a388295a29fe71b133079f5d8b18041cf734c5459c35ad9097af50" dependencies = [ "log", "symphonia-core", @@ -2769,8 +2750,6 @@ dependencies = [ [[package]] name = "symphonia-format-isomp4" version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abfdf178d697e50ce1e5d9b982ba1b94c47218e03ec35022d9f0e071a16dc844" dependencies = [ "encoding_rs", "log", @@ -2782,8 +2761,6 @@ dependencies = [ [[package]] name = "symphonia-format-mkv" version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bb43471a100f7882dc9937395bd5ebee8329298e766250b15b3875652fe3d6f" dependencies = [ "lazy_static", "log", @@ -2795,8 +2772,6 @@ dependencies = [ [[package]] name = "symphonia-format-ogg" version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931" dependencies = [ "log", "symphonia-core", @@ -2807,8 +2782,6 @@ dependencies = [ [[package]] name = "symphonia-format-riff" version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50" dependencies = [ "extended", "log", @@ -2819,8 +2792,6 @@ dependencies = [ [[package]] name = "symphonia-metadata" version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" dependencies = [ "encoding_rs", "lazy_static", @@ -2831,8 +2802,6 @@ dependencies = [ [[package]] name = "symphonia-utils-xiph" version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe" dependencies = [ "symphonia-core", "symphonia-metadata", diff --git a/Cargo.toml b/Cargo.toml index b4e95a5..ec15551 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,13 +18,14 @@ anyhow = "1.0.88" serde = {version = "1.0.210", features = ["derive"]} serde_json = "1.0.128" sled = "0.34.7" -symphonia = { version = "0.5.4", features = ["all"] } +symphonia = { path = "/home/dlj/github/Symphonia/symphonia", features = ["all"] } cfg-if = "1.0.0" tokio = { version = "1.40.0", features = ["full", "tracing"] } futures = { version = "0.3.30", default-features = false } tokio-stream = "0.1.16" chrono = { version = "0.4.38", features = ["serde"]} uuid = { version = "1.8.0", features = ["serde", "v4"] } +ureq = "2.10.1" [profile.release] opt-level = 3 diff --git a/Makefile.toml b/Makefile.toml index 0307e19..8b571b9 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -143,7 +143,7 @@ script = "sudo pkill -9 rsplayer || true" [tasks.run_local] dependencies = ["kill_local"] -env = { "RUST_LOG" = "rsplayer=info,rsplayer_playback=info,warp=info", "RUST_BACKTRACE" = "full", "PORT" = "8000", "TLS_PORT" = "8443", "TLS_CERT_PATH" = "self.crt", "TLS_CERT_KEY_PATH" = "self.key" } +env = { "RUST_LOG" = "rsplayer=info,rsplayer_playback=debug,warp=info", "RUST_BACKTRACE" = "full", "PORT" = "8000", "TLS_PORT" = "8443", "TLS_CERT_PATH" = "self.crt", "TLS_CERT_KEY_PATH" = "self.key" } cwd = ".run" command = "cargo" args = ["run"] diff --git a/rsplayer_metadata/Cargo.toml b/rsplayer_metadata/Cargo.toml index ba26ade..ce982ef 100644 --- a/rsplayer_metadata/Cargo.toml +++ b/rsplayer_metadata/Cargo.toml @@ -15,6 +15,7 @@ sled.workspace = true symphonia.workspace = true chrono.workspace = true uuid.workspace = true +ureq.workspace = true api_models = {path = "../rsplayer_api_models"} walkdir = "2.5.0" diff --git a/rsplayer_metadata/src/queue_service.rs b/rsplayer_metadata/src/queue_service.rs index 7bf9bac..d13a3ae 100644 --- a/rsplayer_metadata/src/queue_service.rs +++ b/rsplayer_metadata/src/queue_service.rs @@ -1,8 +1,12 @@ -use std::sync::{ - atomic::{AtomicBool, AtomicU16, Ordering}, - Arc, +use std::{ + sync::{ + atomic::{AtomicBool, AtomicU16, Ordering}, + Arc, + }, + time::Duration, }; +use log::info; use rand::Rng; use sled::{Db, IVec, Tree}; @@ -72,6 +76,35 @@ impl QueueService { if let Ok(Some(value)) = self.queue_db.get(current_key) { let mut song = Song::bytes_to_song(&value).expect("Failed to parse song"); song.statistics = self.statistics_repository.find_by_id(song.file.as_str()); + if song.file.starts_with("http") { + let agent = ureq::AgentBuilder::new() + .timeout_connect(Duration::from_secs(5)) + .timeout_read(Duration::from_secs(5)) + .timeout_write(Duration::from_secs(5)) + .build(); + let Ok(resp) = agent.get(&song.file).set("accept", "*/*").call() else { + return None; + }; + + let status = resp.status(); + info!("response status code:{status} / status text:{}", resp.status_text()); + if status == 200 { + + resp.headers_names() + .iter() + .filter(|h| h.starts_with("icy-")) + .for_each(|header_key| { + match header_key.as_str() { + "icy-name" => song.title = resp.header(header_key).map(|s|s.to_owned()), + "icy-description" => song.artist = resp.header(header_key).map(|s|s.to_owned()), + "icy-genre" => song.genre = resp.header(header_key).map(|s|s.to_owned()), + _ => () + } + }); + } else { + return None + } + } return Some(song); } } diff --git a/rsplayer_metadata/src/test.rs b/rsplayer_metadata/src/test.rs index e27481d..7dfbd80 100644 --- a/rsplayer_metadata/src/test.rs +++ b/rsplayer_metadata/src/test.rs @@ -1,3 +1,4 @@ +#[cfg(test)] mod queue { use std::sync::Arc; @@ -390,6 +391,15 @@ mod metadata { assert_eq!(stat.liked_count, -2); } + #[test] + fn test_favorite_radio_station(){ + let ctx = TestContext::new(); + ctx.metadata_service.like_media_item("radio_uuid_http://radioaparat.com"); + let favs = ctx.metadata_service.get_favorite_radio_stations(); + assert_eq!(favs.len(), 1); + assert_eq!(favs.get(0).unwrap(), "http://radioaparat.com"); + } + #[test] fn test_increase_play_count() { let ctx = TestContext::new(); @@ -401,6 +411,7 @@ mod metadata { } } +#[cfg(test)] mod playlist { use std::vec; diff --git a/rsplayer_playback/Cargo.toml b/rsplayer_playback/Cargo.toml index f8f94e3..5ea1f97 100644 --- a/rsplayer_playback/Cargo.toml +++ b/rsplayer_playback/Cargo.toml @@ -19,13 +19,13 @@ tokio.workspace = true thread-priority = "1.1.0" core_affinity = "0.8.1" sled.workspace = true - +ureq.workspace = true # symphonia cpal = "0.15.3" # cpal = { path = "/home/dlj/github/cpal" } rb = "0.4.1" # rubato = "0.12.0" -ureq = "2.10.1" + mockall_double = "0.3.1" diff --git a/rsplayer_playback/src/rsp/player_service.rs b/rsplayer_playback/src/rsp/player_service.rs index c662a3f..b8b96ce 100644 --- a/rsplayer_playback/src/rsp/player_service.rs +++ b/rsplayer_playback/src/rsp/player_service.rs @@ -23,16 +23,17 @@ pub struct PlayerService { queue_service: Arc, #[allow(dead_code)] metadata_service: Arc, - play_handle: Arc>>>, - running: Arc, - stopped: Arc, - paused: Arc, + playback_thread_handle: Arc>>>, + stop_signal: Arc, skip_to_time: Arc, audio_device: String, rsp_settings: RsPlayerSettings, music_dir: String, changes_tx: Sender, } +const LAST_SONG_PAUSED_KEY: &str = "last_song_paused"; +const LAST_SONG_PROGRESS_KEY: &str = "last_played_song_progress"; + impl PlayerService { #[must_use] pub fn new( @@ -42,18 +43,28 @@ impl PlayerService { state_changes_tx: Sender, ) -> Self { let db = sled::open("player_state").expect("Failed to open queue db"); - let db2 = db.clone(); + let state_db = db.clone(); let mut rx = state_changes_tx.subscribe(); tokio::task::spawn(async move { let mut i = 0; loop { - if let Ok(StateChangeEvent::SongTimeEvent(st)) = rx.recv().await { - i += 1; - if i % 5 == 0 { - let lt = st.current_time.as_secs().to_string(); - debug!("Save state: {lt}"); - _ = db2.insert("last_played_song_progress", lt.as_bytes()); + match rx.recv().await { + Ok(StateChangeEvent::SongTimeEvent(st)) => { + i += 1; + if i % 2 == 0 { + let lt = st.current_time.as_secs().to_string(); + debug!("Save time state: {lt}"); + _ = state_db.insert(LAST_SONG_PROGRESS_KEY, lt.as_bytes()); + } + } + Ok(StateChangeEvent::PlaybackStateEvent(ps)) => { + debug!("Save player state: {:?}", ps); + _ = match ps { + PlayerState::PLAYING => state_db.remove(LAST_SONG_PAUSED_KEY), + PlayerState::PAUSED | PlayerState::STOPPED => state_db.insert(LAST_SONG_PAUSED_KEY, "true"), + }; } + _ => (), } } }); @@ -62,14 +73,12 @@ impl PlayerService { changes_tx: state_changes_tx, queue_service, metadata_service, - play_handle: Arc::new(Mutex::new(None)), - running: Arc::new(AtomicBool::new(false)), - paused: Arc::new(AtomicBool::new(false)), + playback_thread_handle: Arc::new(Mutex::new(None)), + stop_signal: Arc::new(AtomicBool::new(false)), skip_to_time: Arc::new(AtomicU16::new(0)), audio_device: settings.alsa_settings.output_device.name.clone(), rsp_settings: settings.rs_player_settings.clone(), music_dir: settings.metadata_settings.music_directory.clone(), - stopped: Arc::new(AtomicBool::new(true)), }; let last_played_song_progress = ps.get_last_played_song_time(); if last_played_song_progress > 0 { @@ -79,10 +88,6 @@ impl PlayerService { } pub fn play_from_current_queue_song(&self) { - if self.is_paused() { - let this = self; - this.paused.store(false, Ordering::Relaxed); - } if self.is_playing() { self.changes_tx .send(StateChangeEvent::PlaybackStateEvent(PlayerState::PLAYING)) @@ -92,61 +97,68 @@ impl PlayerService { if let Some(s) = self.queue_service.get_current_song() { self.metadata_service.increase_play_count(&s.file); } + if let Ok(Some(_)) = self.state_db.get(LAST_SONG_PAUSED_KEY) { + let last_song_time = self.get_last_played_song_time(); + self.seek_current_song(last_song_time); + } + *self.playback_thread_handle.lock().unwrap() = Some(self.play_all_in_queue()); } pub fn play_next_song(&self) { - if self.queue_service.move_current_to_next_song() { - self.stop_current_song(); - self.play_from_current_queue_song(); - } + self.stop_current_song(); + self.queue_service.move_current_to_next_song(); + self.play_from_current_queue_song(); } pub fn play_prev_song(&self) { - if self.queue_service.move_current_to_previous_song() { - self.stop_current_song(); - self.play_from_current_queue_song(); - } + self.stop_current_song(); + self.queue_service.move_current_to_previous_song(); + self.play_from_current_queue_song(); } pub fn stop_current_song(&self) { - let this = self; - this.running.store(false, Ordering::Relaxed); + self.stop_signal.store(false, Ordering::Relaxed); self.await_playing_song_to_finish(); } + pub fn pause_current_song(&self) { + if self.is_playing() { + self.stop_current_song(); + } + } + #[allow(clippy::unused_self, clippy::missing_const_for_fn)] pub fn seek_current_song(&self, seconds: u16) { self.skip_to_time.store(seconds, Ordering::Relaxed); } pub fn play_song(&self, song_id: &str) { - if self.queue_service.move_current_to(song_id) { - self.stop_current_song(); - self.play_from_current_queue_song(); - } + self.stop_current_song(); + self.queue_service.move_current_to(song_id); + self.play_from_current_queue_song(); } fn await_playing_song_to_finish(&self) { - while !self.stopped.load(Ordering::Relaxed) { + while self.is_playing() { continue; } debug!("aWait finished"); } - + #[allow(clippy::significant_drop_tightening)] fn is_playing(&self) -> bool { - self.running.load(Ordering::Relaxed) - } - - fn is_paused(&self) -> bool { - self.paused.load(Ordering::Relaxed) + let binding = self.playback_thread_handle.clone(); + let mg = binding.lock().unwrap(); + let handle = mg.as_ref(); + handle.map_or(false, |f| { + let finished = f.is_finished(); + !finished + }) } fn play_all_in_queue(&self) -> JoinHandle { - self.running.store(true, Ordering::Relaxed); - let running = self.running.clone(); - let stopped = self.stopped.clone(); - let paused = self.paused.clone(); + self.stop_signal.store(true, Ordering::Relaxed); + let stop_signal = self.stop_signal.clone(); let skip_to_time = self.skip_to_time.clone(); let queue = self.queue_service.clone(); let audio_device = self.audio_device.clone(); @@ -180,15 +192,15 @@ impl PlayerService { } } let mut num_failed = 0; - let result = loop { + + loop { let Some(song) = queue.get_current_song() else { - running.store(false, Ordering::Relaxed); + stop_signal.store(false, Ordering::Relaxed); changes_tx .send(StateChangeEvent::PlaybackStateEvent(PlayerState::STOPPED)) .ok(); break PlaybackResult::QueueFinished; }; - stopped.store(false, Ordering::Relaxed); changes_tx .send(StateChangeEvent::CurrentSongEvent(song.clone())) .expect("msg send failed"); @@ -197,8 +209,7 @@ impl PlayerService { .expect("msg send failed"); match super::symphonia::play_file( &song.file, - &running, - &paused, + &stop_signal, &skip_to_time, &audio_device, &rsp_settings, @@ -206,7 +217,7 @@ impl PlayerService { &changes_tx, ) { Ok(PlaybackResult::PlaybackStopped) => { - running.store(false, Ordering::Relaxed); + stop_signal.store(false, Ordering::Relaxed); changes_tx .send(StateChangeEvent::PlaybackStateEvent(PlayerState::STOPPED)) .ok(); @@ -217,7 +228,7 @@ impl PlayerService { num_failed += 1; if song.file.starts_with("http") || num_failed == 10 || num_failed >= queue_size { warn!("Number of failed songs is greater than 10. Aborting."); - running.store(false, Ordering::Relaxed); + stop_signal.store(false, Ordering::Relaxed); changes_tx .send(StateChangeEvent::PlaybackStateEvent(PlayerState::STOPPED)) .ok(); @@ -233,15 +244,13 @@ impl PlayerService { if !queue.move_current_to_next_song() { break PlaybackResult::QueueFinished; } - }; - stopped.store(true, Ordering::Relaxed); - result + } }) .unwrap() } fn get_last_played_song_time(&self) -> u16 { - let last_time = match self.state_db.get("last_played_song_progress") { + let last_time = match self.state_db.get(LAST_SONG_PROGRESS_KEY) { Ok(Some(lt)) => { let v = lt.to_vec(); String::from_utf8(v).unwrap() diff --git a/rsplayer_playback/src/rsp/symphonia.rs b/rsplayer_playback/src/rsp/symphonia.rs index 0864645..37ccfcc 100644 --- a/rsplayer_playback/src/rsp/symphonia.rs +++ b/rsplayer_playback/src/rsp/symphonia.rs @@ -2,7 +2,6 @@ use std::fs::File; use std::path::Path; use std::sync::atomic::{AtomicBool, AtomicU16, Ordering}; use std::sync::Arc; -use std::thread::{self}; use std::time::Duration; use anyhow::{format_err, Result}; @@ -37,8 +36,7 @@ unsafe impl Send for PlaybackResult {} #[allow(clippy::type_complexity, clippy::too_many_arguments, clippy::too_many_lines)] pub fn play_file( path_str: &str, - running: &Arc, - paused: &Arc, + stop_signal: &Arc, skip_to_time: &Arc, audio_device: &str, rsp_settings: &RsPlayerSettings, @@ -46,9 +44,10 @@ pub fn play_file( changes_tx: &Sender, ) -> Result { debug!("Playing file {}", path_str); - running.store(true, Ordering::Relaxed); let mut hint = Hint::new(); + let source = get_source(music_dir, path_str, &mut hint)?; + let is_seekable = source.is_seekable(); // Probe the media source stream for metadata and get the format reader. let Ok(probed) = get_probe().format( &hint, @@ -63,7 +62,7 @@ pub fn play_file( ) else { return Err(format_err!("Media source probe failed")); }; - + let mut reader: Box = probed.format; let tracks = reader.tracks(); @@ -94,26 +93,14 @@ pub fn play_file( let decode_opts = &DecoderOptions::default(); let mut decoder = get_codecs().make(codec_parameters, decode_opts)?; let mut audio_output: Option> = None; - let mut paused_time = 0; let mut last_current_time = 0; // Decode and play the packets belonging to the selected track. let loop_result = loop { - if !running.load(Ordering::Relaxed) { + if !stop_signal.load(Ordering::Relaxed) { debug!("Exit from play thread due to running flag change"); break Ok(PlaybackResult::PlaybackStopped); } - let paused = paused.load(Ordering::Relaxed); - if paused { - debug!("Playing paused, going to sleep"); - thread::sleep(Duration::from_millis(300)); - paused_time += 300; - if (paused_time / 1000 / 60) > 5 { - info!("Playing paused for too long, exiting"); - break Ok(PlaybackResult::PlaybackStopped); - } - continue; - } - if skip_to_time.load(Ordering::Relaxed) > 0 { + if is_seekable && skip_to_time.load(Ordering::Relaxed) > 0 { let skip_to = skip_to_time.swap(0, Ordering::Relaxed); debug!("Seeking to {}", skip_to); let seek_result = reader.seek( @@ -138,7 +125,7 @@ pub fn play_file( }; let current_time = tb.calc_time(packet.ts()).seconds; - if !path_str.starts_with("http") && current_time != last_current_time { + if current_time != last_current_time { last_current_time = current_time; changes_tx .send(StateChangeEvent::SongTimeEvent(SongProgress { @@ -200,9 +187,9 @@ pub fn play_file( fn get_source(music_dir: &str, path_str: &str, hint: &mut Hint) -> Result, anyhow::Error> { let source = if path_str.starts_with("http") { let agent = ureq::AgentBuilder::new() - .timeout_connect(Duration::from_secs(10)) - .timeout_read(Duration::from_secs(10)) - .timeout_write(Duration::from_secs(10)) + .timeout_connect(Duration::from_secs(5)) + .timeout_read(Duration::from_secs(5)) + .timeout_write(Duration::from_secs(5)) .build(); let resp = agent.get(path_str).set("accept", "*/*").call()?; let status = resp.status(); diff --git a/rsplayer_web_ui/src/page/music_library_radio.rs b/rsplayer_web_ui/src/page/music_library_radio.rs index faf5e28..e81f8e8 100644 --- a/rsplayer_web_ui/src/page/music_library_radio.rs +++ b/rsplayer_web_ui/src/page/music_library_radio.rs @@ -1,3 +1,5 @@ +use std::vec; + use api_models::common::{MetadataCommand, QueueCommand, UserCommand}; use api_models::state::StateChangeEvent; use gloo_net::http::Request; @@ -62,6 +64,7 @@ pub enum Msg { UnfavoriteRadioStation(NodeId), AddItemToQueue(NodeId), LoadItemToQueue(NodeId), + LoadAllItemsToQueue, CountriesFetched(Vec), LanguagesFetched(Vec), StationsFetched(Vec), @@ -82,6 +85,7 @@ pub enum FilterType { Country, Language, Tag, + Search, } #[derive(Debug)] @@ -143,6 +147,9 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { MetadataCommand::QueryFavoriteRadioStations, ))); } + FilterType::Search => { + orders.send_msg(Msg::DoSearch); + } } model.wait_response = true; model.filter_type = filter_type; @@ -173,9 +180,17 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { model.tree.current.append(node, &mut model.tree.arena); }); } - Msg::StatusChangeEventReceived(StateChangeEvent::FavoriteRadioStations(event)) => { + StationsFetched(list) => { model.wait_response = false; model.tree = TreeModel::new(); + list.into_iter().for_each(|item| { + let node = model.tree.arena.new_node(TreeNode::Station(item)); + model.tree.current.append(node, &mut model.tree.arena); + }); + } + Msg::StatusChangeEventReceived(StateChangeEvent::FavoriteRadioStations(event)) => { + model.wait_response = false; + // model.tree = TreeModel::new(); orders.perform_cmd(async { StationsFetched(fetch_stations_by_uuid(event).await) }); } @@ -209,13 +224,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { c.remove_subtree(&mut model.tree.arena); } } - StationsFetched(list) => { - model.wait_response = false; - list.into_iter().for_each(|item| { - let node = model.tree.arena.new_node(TreeNode::Station(item)); - model.tree.current.append(node, &mut model.tree.arena); - }); - } + AddItemToQueue(id) => { let node = model.tree.arena.get(id).unwrap().get(); if let TreeNode::Station(station) = node { @@ -232,6 +241,15 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { )))); } } + Msg::LoadAllItemsToQueue => { + model.tree.arena.iter().for_each(|node| { + if let TreeNode::Station(station) = node.get() { + orders.send_msg(Msg::SendUserCommand(UserCommand::Queue(QueueCommand::AddSongToQueue( + station.url.clone(), + )))); + } + }); + } Msg::FavoriteRadioStation(id) => { let node = model.tree.arena.get(id).unwrap().get(); if let TreeNode::Station(station) = node { @@ -255,7 +273,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { } Msg::DoSearch => { model.wait_response = true; - model.tree = TreeModel::new(); + model.filter_type = FilterType::Search; let search_term = model.search_input.clone(); orders.perform_cmd(async move { StationsFetched(search_stations_by_name(&search_term).await) }); } @@ -278,12 +296,7 @@ pub fn view(model: &Model) -> Node { view_search_input(model), ul![ C!["wtree"], - get_tree_start_node( - model.tree.root, - &model.tree.arena, - &model.filter_type, - !model.search_input.is_empty() - ) + view_tree(model.tree.root, &model.tree.arena, &model.filter_type) ] ] } @@ -314,15 +327,26 @@ fn view_search_input(model: &Model) -> Node { div![ C!["control"], a![ + C!["ml-2"], attrs!(At::Title =>"Search"), i![C!["material-icons", "is-large-icon", "white-icon"], "search"], ev(Ev::Click, move |_| Msg::DoSearch) ], a![ - attrs!(At::Title =>"Clear search / Show all songs"), + C!["ml-2"], + attrs!(At::Title =>"Clear search"), i![C!["material-icons", "is-large-icon", "white-icon"], "backspace"], ev(Ev::Click, move |_| Msg::ClearSearch) ], + a![ + C!["ml-4"], + attrs!(At::Title =>"Add all items to queue"), + i![ + C!["material-icons", "is-large-icon", "white-icon"], + "playlist_add" + ], + ev(Ev::Click, move |_| Msg::LoadAllItemsToQueue) + ], ], ] } @@ -382,12 +406,7 @@ fn view_filter(filter_type: &FilterType) -> Node { } #[allow(clippy::collection_is_never_read)] -fn get_tree_start_node( - node_id: NodeId, - arena: &Arena, - filter_type: &FilterType, - is_search_mode: bool, -) -> Node { +fn view_tree(node_id: NodeId, arena: &Arena, filter_type: &FilterType) -> Node { let Some(value) = arena.get(node_id) else { return empty!(); }; @@ -434,7 +453,7 @@ fn get_tree_start_node( style! { St::Height => node_height, }, - IF!(is_root && !is_search_mode => view_filter(filter_type)), + IF!(is_root => view_filter(filter_type)), IF!(is_root => style! { St::Padding => "5px" }), ]; @@ -521,7 +540,7 @@ fn get_tree_start_node( if !children.is_empty() { let mut ul: Node = ul!(); for c in children { - ul.add_child(get_tree_start_node(c, arena, filter_type, is_search_mode)); + ul.add_child(view_tree(c, arena, filter_type)); } li.add_child(ul); } @@ -530,6 +549,7 @@ fn get_tree_start_node( const RADIO_BROWSER_URL: &str = "https://de1.api.radio-browser.info/json/"; +#[allow(clippy::future_not_send)] async fn search_stations_by_name(name: &str) -> Vec { let url = format!( "{}stations/search?name={}&limit=300&hidebroken=true", @@ -543,65 +563,55 @@ async fn search_stations_by_name(name: &str) -> Vec { .await .unwrap() } + +#[allow(clippy::future_not_send)] async fn fetch_countries() -> Vec { let url = format!("{}countries?limit=200&hidebroken=true", RADIO_BROWSER_URL); - Request::get(&url) - .send() - .await - .unwrap() - .json::>() - .await - .unwrap() + let Ok(response) = Request::get(&url).send().await else { + return vec![]; + }; + response.json::>().await.unwrap() } +#[allow(clippy::future_not_send)] async fn fetch_stations_by_uuid(uuids: Vec) -> Vec { let url = format!("{}stations/byuuid?uuids={}", RADIO_BROWSER_URL, uuids.join(",")); - Request::get(&url) - .send() - .await - .unwrap() - .json::>() - .await - .unwrap() + let Ok(response) = Request::get(&url).send().await else { + return vec![]; + }; + response.json::>().await.unwrap() } +#[allow(clippy::future_not_send)] async fn fetch_stations(by: &str, value: &str) -> Vec { let url = format!( "{}stations/{by}/{}?limit=300&hidebroken=true&order=votes&reverse=true", RADIO_BROWSER_URL, value ); - Request::get(&url) - .send() - .await - .unwrap() - .json::>() - .await - .unwrap() + let Ok(response) = Request::get(&url).send().await else { + return vec![]; + }; + response.json::>().await.unwrap() } - +#[allow(clippy::future_not_send)] async fn fetch_languages() -> Vec { let url = format!("{}languages?limit=500", RADIO_BROWSER_URL); - Request::get(&url) - .send() - .await - .unwrap() - .json::>() - .await - .unwrap() + let Ok(response) = Request::get(&url).send().await else { + return vec![]; + }; + response.json::>().await.unwrap() } - +#[allow(clippy::future_not_send)] async fn fetch_tags() -> Vec { let url = format!( "{}tags?limit=500&order=stationcount&reverse=true&hidebroken=true", RADIO_BROWSER_URL ); - Request::get(&url) - .send() - .await - .unwrap() - .json::>() - .await - .unwrap() + + let Ok(response) = Request::get(&url).send().await else { + return vec![]; + }; + response.json::>().await.unwrap() } #[cfg(test)]