From 769cec8dbe49f94498fb111cad9996483c638c49 Mon Sep 17 00:00:00 2001 From: "Jonas H." Date: Mon, 13 Jul 2026 17:27:44 +0200 Subject: [PATCH] add play-all --- cli/__pycache__/sjnmusiccpython-312.pyc | Bin 0 -> 34302 bytes cli/sjnmusic | 9 ++++++ daemon/src/server.rs | 11 ++++++++ daemon/src/state.rs | 34 ++++++++++++++++++++++ gui-rs/src/daemon.rs | 11 ++++++++ gui-rs/src/views/library.rs | 36 ++++++++++++++++++++++++ 6 files changed, 101 insertions(+) create mode 100644 cli/__pycache__/sjnmusiccpython-312.pyc diff --git a/cli/__pycache__/sjnmusiccpython-312.pyc b/cli/__pycache__/sjnmusiccpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..995fda53d7d5bda5fb8ddcad86f03a19c16e47a5 GIT binary patch literal 34302 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 argparse.ArgumentParser: sub.add_parser("resume", help="resume playback") sub.add_parser("shuffle", help="shuffle the queue in place") sub.add_parser("clear", help="empty the queue") + sub.add_parser( + "play-all", + help="shuffle the entire library into the queue and start playing", + ) sub.add_parser("songs", help="list all known songs") sub.add_parser("playlists", help="list all playlists") sub.add_parser("downloads", help="list recent yt-dlp download jobs") @@ -537,6 +541,9 @@ def print_response(args, status, payload): else: print("now playing: (idle)") return + if isinstance(obj, dict) and "queued" in obj: + print(f"queued {obj['queued']} song(s) in random order") + return if isinstance(obj, dict) and "songs" in obj: for s in obj["songs"]: print(f"[{s['id']}] {s['name']}") @@ -642,6 +649,8 @@ def main(argv=None): status, payload = request(args, host, port, "POST", "/queue/shuffle") elif args.cmd == "clear": status, payload = request(args, host, port, "POST", "/queue/clear") + elif args.cmd == "play-all": + status, payload = request(args, host, port, "POST", "/play/all", body={}) elif args.cmd == "songs": status, payload = request(args, host, port, "GET", "/songs") elif args.cmd == "downloads": diff --git a/daemon/src/server.rs b/daemon/src/server.rs index ccf4db3..a04f080 100644 --- a/daemon/src/server.rs +++ b/daemon/src/server.rs @@ -132,6 +132,7 @@ fn handle( "POST /del {query}", "POST /init {song} (async, returns job id, status 202)", "POST /init/batch {songs:[...]}", + "POST /play/all", "POST /skip", "POST /pause", "POST /resume", @@ -412,6 +413,14 @@ fn handle( Err(e) => error_response(e), }, + Route::PlayAll => match state.play_all_random() { + Ok(len) => json_response( + 200, + &serde_json::json!({ "queued": len }), + ), + Err(e) => error_response(e), + }, + Route::Skip => { state.audio_handle.send(AudioCmd::Stop); // Always clear current — a skip is an explicit decision to @@ -688,6 +697,7 @@ enum Route { Del, InitAsync, InitBatch, + PlayAll, Skip, QueueShuffle, QueueClear, @@ -731,6 +741,7 @@ fn route_for(method: &Method, path: &str) -> Route { (Method::Post, "/del") => Route::Del, (Method::Post, "/init") => Route::InitAsync, (Method::Post, "/init/batch") => Route::InitBatch, + (Method::Post, "/play/all") => Route::PlayAll, (Method::Post, "/skip") => Route::Skip, (Method::Post, "/queue/shuffle") => Route::QueueShuffle, (Method::Post, "/queue/clear") => Route::QueueClear, diff --git a/daemon/src/state.rs b/daemon/src/state.rs index 74f02e7..f141f29 100644 --- a/daemon/src/state.rs +++ b/daemon/src/state.rs @@ -450,6 +450,40 @@ impl DaemonState { Ok(song) } + /// Replace the queue with a shuffled copy of the entire library. + /// + /// Mirrors `/playlists/{name}/play`: clear the queue, re-insert in + /// random order, persist to the DB, and refresh `cycle_snapshot` so + /// `RepeatMode::All` keeps looping over the same shuffle. The daemon + /// tick loop picks up the new head of queue and starts playback + /// automatically once the audio engine is idle (same model as + /// `/play`); calling `play_all_random` while a song is playing only + /// queues the rest of the library behind the current track. + /// + /// Returns the number of songs queued. An empty library surfaces + /// `"library is empty"` to the HTTP layer so the GUI/CLI can render + /// a useful toast instead of silently doing nothing. + pub fn play_all_random(&mut self) -> Result { + let mut shuffled = self.all_songs(); + if shuffled.is_empty() { + return Err("library is empty".to_string()); + } + use rand::seq::SliceRandom; + shuffled.shuffle(&mut rand::thread_rng()); + { + let conn = self.lock_conn(); + Self::queue_clear_db(&conn).map_err(|e| e.to_string())?; + for s in &shuffled { + Self::queue_append_db(&conn, s.id).map_err(|e| e.to_string())?; + } + } + let len = shuffled.len(); + self.queue = shuffled; + self.cycle_snapshot = self.queue.clone(); + info!("Play-all: queued {len} songs in random order"); + Ok(len) + } + /// In-place shuffle of the current queue. pub fn shuffle_queue(&mut self) -> Result<(), String> { if self.queue.len() < 2 { diff --git a/gui-rs/src/daemon.rs b/gui-rs/src/daemon.rs index 5ff0532..f0f1df4 100644 --- a/gui-rs/src/daemon.rs +++ b/gui-rs/src/daemon.rs @@ -282,6 +282,11 @@ pub struct ClearedResponse { pub cleared: bool, } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PlayAllResponse { + pub queued: usize, +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct OkResponse { pub ok: bool, @@ -539,6 +544,12 @@ impl DaemonClient { self.post_json("/queue/clear", &serde_json::json!({})) } + /// Replace the queue with the entire library in random order. + /// Server returns the number of songs queued (`{"queued": N}`). + pub fn play_all(&self) -> Result { + self.post_json("/play/all", &serde_json::json!({})) + } + pub fn init(&self, name: &str, url: Option<&str>) -> Result { let mut body = serde_json::json!({"song": name, "name": name}); if let Some(u) = url { diff --git a/gui-rs/src/views/library.rs b/gui-rs/src/views/library.rs index 22f5e76..bc9a158 100644 --- a/gui-rs/src/views/library.rs +++ b/gui-rs/src/views/library.rs @@ -17,6 +17,23 @@ pub fn show(ui: &mut egui::Ui, app: &mut SJNMusicApp) { ui.horizontal(|ui| { ui.heading("Library"); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + // ▶ Play all (shuffle): replace the queue with the whole + // library in random order. Disabled while the daemon is + // unreachable so the user doesn't get a silent no-op click, + // disabled while the first library fetch is still in flight + // (so the user sees a "loading…" message instead of a + // silently dead button), and disabled until the fetch has + // returned a non-empty result (so a real empty library + // doesn't pretend to have something to shuffle). + let can_play_all = app.state.is_online() + && !app.library_loading + && !app.library_songs.is_empty(); + if ui + .add_enabled(can_play_all, egui::Button::new("▶ Play all (shuffle)")) + .clicked() + { + play_all_now(app); + } let response = ui.add( egui::TextEdit::singleline(&mut app.library_query) .hint_text("Filter by name (fuzzy)") @@ -114,6 +131,25 @@ fn row(ui: &mut egui::Ui, song: &Song, app: &mut SJNMusicApp) { }); } +/// Send `POST /play/all` to the daemon and surface the result as a +/// toast. The call is synchronous but cheap (loopback + ~10 ms server +/// time), matching how `play_playlist`, `create_playlist`, etc. are +/// handled elsewhere — fire-and-forget would lose the queued-count +/// success message and the daemon's "library is empty" rejection. +fn play_all_now(app: &mut SJNMusicApp) { + let daemon: Arc = app.daemon.clone(); + match daemon.play_all() { + Ok(resp) => app.push_toast( + format!("Shuffled {} song(s) into the queue", resp.queued), + crate::state::ToastLevel::Success, + ), + Err(e) => app.push_toast( + format!("Play-all failed: {e}"), + crate::state::ToastLevel::Error, + ), + } +} + /// First-paint fetch. We set `library_loading` BEFORE the call so a /// fast double-click doesn't enter twice. We always push SOMETHING into /// the bus (empty list on Err) so `drain_background_results` can clear