From 22dab3e3eeced7babdb293dd6dd83929924482b3 Mon Sep 17 00:00:00 2001 From: cwj3rcb Date: Sat, 14 Feb 2026 17:57:20 -0600 Subject: [PATCH] half way working autoplay --- src/main.rs | 171 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 165 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index cd09497..57e2c54 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ mod core; const PAGE_SIZE: usize = 50; use std::{ - io::{self, Write}, + io::{self, Write, BufRead, BufReader}, path::PathBuf, process::{Child, Command, Stdio}, time::{Duration, Instant}, @@ -152,6 +152,10 @@ struct App { search: Option, mpv: Option, + + autoplay: bool, + autoplay_view_depth: usize, + autoplay_next_index: Option, } #[derive(Debug)] @@ -207,6 +211,9 @@ impl Default for App { login_error: None, search: None, mpv: None, + autoplay: false, + autoplay_view_depth: 0, + autoplay_next_index: None, } } } @@ -253,14 +260,31 @@ fn run_app(terminal: &mut Terminal>) -> io::Result< } } - if let Some(mpv) = &mut app.mpv { - if let Ok(Some(status)) = mpv.child.try_wait() { - app.status = format!("mpv exited: {status}"); + // --- mpv lifecycle (no borrow issues) --- + let mpv_exited = match app.mpv.as_mut() { + Some(mpv) => match mpv.child.try_wait() { + Ok(Some(status)) => Some(status), + _ => None, + }, + None => None, + }; + + if let Some(status) = mpv_exited { + // take ownership so we can clean up + if let Some(mpv) = app.mpv.take() { let _ = std::fs::remove_file(&mpv.socket_path); - app.mpv = None; + app.status = format!("mpv exited: {status}"); } + + // Your requirement: window closing disables autoplay + app.autoplay = false; + app.autoplay_next_index = None; } + // Option A: always tick autoplay by polling eof-reached (window stays open) + autoplay_tick(&mut app); + + if last_tick.elapsed() >= tick_rate { // tick-based updates go here later (smooth nav, download polling, etc.) last_tick = Instant::now(); @@ -418,9 +442,10 @@ fn handle_key(app: &mut App, code: KeyCode) { other => other, }; - // q behavior: if not home, go home; if home, quit if matches!(code, KeyCode::Char('q')) { + app.autoplay = false; + app.autoplay_next_index = None; if !matches!(app.current_view(), View::Categories) { app.view_stack = vec![View::Categories]; app.selected = 0; @@ -460,6 +485,8 @@ fn handle_key(app: &mut App, code: KeyCode) { return; } KeyCode::Char('x') => { + app.autoplay = false; + app.autoplay_next_index = None; if let Some(mut mpv) = app.mpv.take() { mpv_stop(&mut mpv); app.status = "Stopped mpv".into(); @@ -487,6 +514,9 @@ fn handle_key(app: &mut App, code: KeyCode) { return; } KeyCode::Char('p') => { + app.autoplay = false; + app.autoplay_next_index = None; + // p = play selected item (spawn or replace) let cfg = match &app.config { Some(c) => c.clone(), @@ -513,6 +543,34 @@ fn handle_key(app: &mut App, code: KeyCode) { } return; } + KeyCode::Char('P') => { + let cfg = match &app.config { Some(c) => c.clone(), None => { app.status="No config".into(); return; } }; + + let (item_id, selected, len) = match app.current_view() { + View::Library { items, selected, .. } if !items.is_empty() => { + (items[*selected].id.clone(), *selected, items.len()) + } + _ => { app.status = "Nothing to play here".into(); return; } + }; + + app.autoplay = true; + app.autoplay_view_depth = app.view_stack.len(); + app.autoplay_next_index = if selected + 1 < len { Some(selected + 1) } else { None }; + + // load/replace or spawn + if let Some(mpv) = &app.mpv { + match mpv_load_item(&cfg, mpv, &item_id) { + Ok(_) => app.status = "mpv: playing (autoplay)".into(), + Err(e) => app.status = format!("mpv load failed: {e}"), + } + } else { + match spawn_mpv(&cfg, &item_id) { + Ok(mpv) => { app.mpv = Some(mpv); app.status = "mpv: playing (autoplay)".into(); } + Err(e) => app.status = format!("mpv spawn failed: {e}"), + } + } + return; + } _ => {} } } @@ -1154,6 +1212,38 @@ fn mpv_socket_path() -> PathBuf { p } +fn mpv_request(socket: &PathBuf, json_line: &str) -> Result { + let mut stream = UnixStream::connect(socket) + .map_err(|e| format!("mpv ipc connect failed: {e}"))?; + + stream + .write_all(json_line.as_bytes()) + .map_err(|e| format!("mpv ipc write failed: {e}"))?; + // mpv replies with one JSON line per request + let mut reader = BufReader::new(stream); + let mut line = String::new(); + reader + .read_line(&mut line) + .map_err(|e| format!("mpv ipc read failed: {e}"))?; + + Ok(line) +} + +fn mpv_get_bool(mpv: &MpvSession, prop: &str) -> Result { + let resp = mpv_request( + &mpv.socket_path, + &format!(r#"{{"command":["get_property","{}"]}}\n"#, prop), + )?; + + // Response looks like: {"data":false,"error":"success"}\n + if !resp.contains(r#""error":"success""#) { + return Err(format!("mpv get_property failed: {}", resp.trim())); + } + + // Very small parser: good enough for booleans. + Ok(resp.contains(r#""data":true"#)) +} + fn mpv_send(socket: &PathBuf, json_line: &str) -> Result<(), String> { let mut stream = UnixStream::connect(socket) .map_err(|e| format!("mpv ipc connect failed: {e}"))?; @@ -1252,6 +1342,75 @@ fn mpv_cycle_pause(mpv: &MpvSession) -> Result<(), String> { mpv_cmd(&mpv.socket_path, r#"{"command":["cycle","pause"]}"#) } +fn autoplay_tick(app: &mut App) { + if !app.autoplay { + return; + } + + // Must still be on the same "list" view depth we started autoplay from + if app.view_stack.len() != app.autoplay_view_depth { + app.autoplay = false; + app.autoplay_next_index = None; + return; + } + + let mpv = match &app.mpv { + Some(m) => m, + None => return, + }; + + // Only advance when mpv reports EOF reached + let eof = match mpv_get_bool(mpv, "eof-reached") { + Ok(v) => v, + Err(_e) => return, // don't spam status; just skip this tick + }; + + if !eof { + return; + } + + let next_i = match app.autoplay_next_index { + Some(i) => i, + None => { + app.autoplay = false; + app.status = "autoplay finished".into(); + return; + } + }; + + // Grab next id from current library view + let (next_id, len) = match app.current_view() { + View::Library { items, .. } if next_i < items.len() => (items[next_i].id.clone(), items.len()), + _ => { + app.autoplay = false; + app.autoplay_next_index = None; + app.status = "autoplay stopped".into(); + return; + } + }; + + // update selection so UI follows playback + if let Some(View::Library { selected, .. }) = app.view_stack.last_mut() { + *selected = next_i; + } + + // set following next index + app.autoplay_next_index = if next_i + 1 < len { Some(next_i + 1) } else { None }; + + // Load next item in the *same* mpv window + if let Some(cfg) = app.config.clone() { + if let Some(mpv) = &app.mpv { + if let Err(e) = mpv_load_item(&cfg, mpv, &next_id) { + app.autoplay = false; + app.autoplay_next_index = None; + app.status = format!("autoplay load failed: {e}"); + } else { + app.status = "autoplay next".into(); + } + } + } +} + struct Panes { top: Rect, left_main: Rect,