half way working autoplay
This commit is contained in:
171
src/main.rs
171
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<SearchState>,
|
||||
mpv: Option<MpvSession>,
|
||||
|
||||
autoplay: bool,
|
||||
autoplay_view_depth: usize,
|
||||
autoplay_next_index: Option<usize>,
|
||||
}
|
||||
|
||||
#[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<CrosstermBackend<io::Stdout>>) -> 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<String, String> {
|
||||
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<bool, String> {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user