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;
|
const PAGE_SIZE: usize = 50;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
io::{self, Write},
|
io::{self, Write, BufRead, BufReader},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
process::{Child, Command, Stdio},
|
process::{Child, Command, Stdio},
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
@@ -152,6 +152,10 @@ struct App {
|
|||||||
|
|
||||||
search: Option<SearchState>,
|
search: Option<SearchState>,
|
||||||
mpv: Option<MpvSession>,
|
mpv: Option<MpvSession>,
|
||||||
|
|
||||||
|
autoplay: bool,
|
||||||
|
autoplay_view_depth: usize,
|
||||||
|
autoplay_next_index: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -207,6 +211,9 @@ impl Default for App {
|
|||||||
login_error: None,
|
login_error: None,
|
||||||
search: None,
|
search: None,
|
||||||
mpv: 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 {
|
// --- mpv lifecycle (no borrow issues) ---
|
||||||
if let Ok(Some(status)) = mpv.child.try_wait() {
|
let mpv_exited = match app.mpv.as_mut() {
|
||||||
app.status = format!("mpv exited: {status}");
|
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);
|
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 {
|
if last_tick.elapsed() >= tick_rate {
|
||||||
// tick-based updates go here later (smooth nav, download polling, etc.)
|
// tick-based updates go here later (smooth nav, download polling, etc.)
|
||||||
last_tick = Instant::now();
|
last_tick = Instant::now();
|
||||||
@@ -418,9 +442,10 @@ fn handle_key(app: &mut App, code: KeyCode) {
|
|||||||
other => other,
|
other => other,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// q behavior: if not home, go home; if home, quit
|
// q behavior: if not home, go home; if home, quit
|
||||||
if matches!(code, KeyCode::Char('q')) {
|
if matches!(code, KeyCode::Char('q')) {
|
||||||
|
app.autoplay = false;
|
||||||
|
app.autoplay_next_index = None;
|
||||||
if !matches!(app.current_view(), View::Categories) {
|
if !matches!(app.current_view(), View::Categories) {
|
||||||
app.view_stack = vec![View::Categories];
|
app.view_stack = vec![View::Categories];
|
||||||
app.selected = 0;
|
app.selected = 0;
|
||||||
@@ -460,6 +485,8 @@ fn handle_key(app: &mut App, code: KeyCode) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
KeyCode::Char('x') => {
|
KeyCode::Char('x') => {
|
||||||
|
app.autoplay = false;
|
||||||
|
app.autoplay_next_index = None;
|
||||||
if let Some(mut mpv) = app.mpv.take() {
|
if let Some(mut mpv) = app.mpv.take() {
|
||||||
mpv_stop(&mut mpv);
|
mpv_stop(&mut mpv);
|
||||||
app.status = "Stopped mpv".into();
|
app.status = "Stopped mpv".into();
|
||||||
@@ -487,6 +514,9 @@ fn handle_key(app: &mut App, code: KeyCode) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
KeyCode::Char('p') => {
|
KeyCode::Char('p') => {
|
||||||
|
app.autoplay = false;
|
||||||
|
app.autoplay_next_index = None;
|
||||||
|
|
||||||
// p = play selected item (spawn or replace)
|
// p = play selected item (spawn or replace)
|
||||||
let cfg = match &app.config {
|
let cfg = match &app.config {
|
||||||
Some(c) => c.clone(),
|
Some(c) => c.clone(),
|
||||||
@@ -513,6 +543,34 @@ fn handle_key(app: &mut App, code: KeyCode) {
|
|||||||
}
|
}
|
||||||
return;
|
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
|
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> {
|
fn mpv_send(socket: &PathBuf, json_line: &str) -> Result<(), String> {
|
||||||
let mut stream = UnixStream::connect(socket)
|
let mut stream = UnixStream::connect(socket)
|
||||||
.map_err(|e| format!("mpv ipc connect failed: {e}"))?;
|
.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"]}"#)
|
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 {
|
struct Panes {
|
||||||
top: Rect,
|
top: Rect,
|
||||||
left_main: Rect,
|
left_main: Rect,
|
||||||
|
|||||||
Reference in New Issue
Block a user