diff options
-rw-r--r-- | crates/yt/src/cli.rs | 10 | ||||
-rw-r--r-- | crates/yt/src/main.rs | 7 | ||||
-rw-r--r-- | crates/yt/src/watch/mod.rs | 89 | ||||
-rw-r--r-- | crates/yt/src/watch/playlist.rs | 14 | ||||
-rw-r--r-- | crates/yt/src/watch/playlist_handler/mod.rs | 135 |
5 files changed, 89 insertions, 166 deletions
diff --git a/crates/yt/src/cli.rs b/crates/yt/src/cli.rs index 8892efd..dc200eb 100644 --- a/crates/yt/src/cli.rs +++ b/crates/yt/src/cli.rs @@ -98,7 +98,15 @@ pub(crate) enum Command { }, /// Watch the already cached (and selected) videos - Watch {}, + Watch { + /// Print the path to an ipc socket for mpv control to stdout at startup. + #[arg(long)] + provide_ipc_socket: bool, + + /// Don't start an mpv window at all. + #[arg(long)] + headless: bool, + }, /// Visualize the current playlist Playlist { diff --git a/crates/yt/src/main.rs b/crates/yt/src/main.rs index 824f842..fcbe0da 100644 --- a/crates/yt/src/main.rs +++ b/crates/yt/src/main.rs @@ -273,7 +273,10 @@ async fn main() -> Result<()> { } }, - Command::Watch {} => watch::watch(Arc::new(app)).await?, + Command::Watch { + provide_ipc_socket, + headless, + } => watch::watch(Arc::new(app), provide_ipc_socket, headless).await?, Command::Playlist { watch } => watch::playlist::playlist(&app, watch).await?, Command::Status {} => status::show(&app).await?, @@ -310,7 +313,7 @@ async fn dowa(arc_app: Arc<App>) -> Result<()> { } }); - watch::watch(arc_app).await?; + watch::watch(arc_app, false, false).await?; download.await?; Ok(()) } diff --git a/crates/yt/src/watch/mod.rs b/crates/yt/src/watch/mod.rs index 1936d48..8c7d6f8 100644 --- a/crates/yt/src/watch/mod.rs +++ b/crates/yt/src/watch/mod.rs @@ -10,47 +10,65 @@ // If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. use std::{ + fs, + path::PathBuf, sync::{ Arc, atomic::{AtomicBool, Ordering}, }, - time::Duration, }; use anyhow::{Context, Result}; use libmpv2::{Mpv, events::EventContext}; use log::{debug, info, trace, warn}; -use playlist_handler::{reload_mpv_playlist, save_watch_progress}; -use tokio::{task, time::sleep}; +use tokio::{task, time}; use self::playlist_handler::Status; use crate::{ app::App, cache::maintain, - storage::video_database::{get, notify::wait_for_db_write}, + storage::{ + db::{insert::Operations, playlist::Playlist}, + notify::wait_for_db_write, + }, }; -pub mod playlist; -pub mod playlist_handler; +pub(crate) mod playlist; +pub(crate) mod playlist_handler; -fn init_mpv(app: &App) -> Result<(Mpv, EventContext)> { +fn init_mpv(app: &App, ipc_socket: Option<PathBuf>, headless: bool) -> Result<(Mpv, EventContext)> { // set some default values, to make things easier (these can be overridden by the config file, // which we load later) let mpv = Mpv::with_initializer(|mpv| { - // Enable default key bindings, so the user can actually interact with - // the player (and e.g. close the window). - mpv.set_property("input-default-bindings", "yes")?; - mpv.set_property("input-vo-keyboard", "yes")?; + if let Some(socket) = ipc_socket { + mpv.set_property( + "input-ipc-server", + socket + .to_str() + .expect("This path comes from us, it should never contain not-utf8"), + )?; + } + + if headless { + // Do not provide video output. + mpv.set_property("vid", "no")?; + } else { + // Enable default key bindings, so the user can actually interact with + // the player (and e.g. close the window). + mpv.set_property("input-default-bindings", "yes")?; + mpv.set_property("input-vo-keyboard", "yes")?; - // Show the on screen controller. - mpv.set_property("osc", "yes")?; + // Show the on screen controller. + mpv.set_property("osc", "yes")?; - // Don't automatically advance to the next video (or exit the player) - mpv.set_option("keep-open", "always")?; + // Don't automatically advance to the next video (or exit the player) + mpv.set_option("keep-open", "always")?; + + // Always display an window, even for non-video playback. + // As mpv does not have cli access, no window means no control and no user feedback. + mpv.set_option("force-window", "yes")?; + } - // Always display an window, even for non-video playback. - // As mpv does not have cli access, no window means no control and no user feedback. - mpv.set_option("force-window", "yes")?; Ok(()) }) .context("Failed to initialize mpv")?; @@ -93,12 +111,23 @@ fn init_mpv(app: &App) -> Result<(Mpv, EventContext)> { Ok((mpv, ev_ctx)) } -pub(crate) async fn watch(app: Arc<App>) -> Result<()> { - maintain(&app, false).await?; +pub(crate) async fn watch(app: Arc<App>, provide_ipc_socket: bool, headless: bool) -> Result<()> { + maintain(&app).await?; - let (mpv, mut ev_ctx) = init_mpv(&app).context("Failed to initialize mpv instance")?; + let ipc_socket = if provide_ipc_socket { + Some(app.config.paths.mpv_ipc_socket_path.clone()) + } else { + None + }; + + let (mpv, mut ev_ctx) = + init_mpv(&app, ipc_socket, headless).context("Failed to initialize mpv instance")?; let mpv = Arc::new(mpv); - reload_mpv_playlist(&app, &mpv, None, None).await?; + + // We now _know_ that the socket is set-up and ready. + if provide_ipc_socket { + println!("{}", app.config.paths.mpv_ipc_socket_path.display()); + } let should_break = Arc::new(AtomicBool::new(false)); @@ -173,10 +202,13 @@ pub(crate) async fn watch(app: Arc<App>) -> Result<()> { } } - if let Some(ev) = ev_ctx.wait_event(30.) { + // TODO(@bpeetz): Is the following assumption correct? <2025-07-10> + // We wait until forever for the next event, because we really don't need to do anything + // else. + if let Some(ev) = ev_ctx.wait_event(f64::MAX) { match ev { Ok(event) => { - trace!("Mpv event triggered: {:#?}", event); + trace!("Mpv event triggered: {event:#?}"); if playlist_handler::handle_mpv_event(&app, &mpv, &event) .await .with_context(|| format!("Failed to handle mpv event: '{event:#?}'"))? @@ -192,5 +224,14 @@ pub(crate) async fn watch(app: Arc<App>) -> Result<()> { should_break.store(true, Ordering::Relaxed); progress_handle.await??; + if provide_ipc_socket { + fs::remove_file(&app.config.paths.mpv_ipc_socket_path).with_context(|| { + format!( + "Failed to clean-up the mpve ipc socket at {}", + app.config.paths.mpv_ipc_socket_path.display() + ) + })?; + } + Ok(()) } diff --git a/crates/yt/src/watch/playlist.rs b/crates/yt/src/watch/playlist.rs index 0bd8f2e..7f1db2b 100644 --- a/crates/yt/src/watch/playlist.rs +++ b/crates/yt/src/watch/playlist.rs @@ -69,11 +69,17 @@ pub(crate) async fn playlist(app: &App, watch: bool) -> Result<()> { if is_focused { output.push_str(" ("); - output.push_str(&if let Some(duration) = video.duration.as_secs() { - format!("{:0.0}%", (video.watch_progress.as_secs() / duration) * 100) + if let Some(duration) = video.duration.as_secs() { + let watch_progress: f64 = f64::from( + u32::try_from(video.watch_progress.as_secs()).expect("No overflow"), + ); + let duration = f64::from(u32::try_from(duration).expect("No overflow")); + + write!(output, "{:0.0}%", (watch_progress / duration) * 100.0)?; } else { - video.watch_progress_fmt(app) - }); + write!(output, "{}", video.watch_progress_fmt(app))?; + } + output.push(')'); } output.push(']'); diff --git a/crates/yt/src/watch/playlist_handler/mod.rs b/crates/yt/src/watch/playlist_handler/mod.rs index 71f277c..443fd26 100644 --- a/crates/yt/src/watch/playlist_handler/mod.rs +++ b/crates/yt/src/watch/playlist_handler/mod.rs @@ -47,141 +47,6 @@ fn mpv_message(mpv: &Mpv, message: &str, time: Duration) -> Result<()> { Ok(()) } -async fn apply_video_options(app: &App, mpv: &Mpv, video: &ExtractorHash) -> Result<()> { - let options = get::video_mpv_opts(app, video).await?; - let video = get::video_by_hash(app, video).await?; - - mpv.set_property("speed", options.playback_speed)?; - - // We already start at 0, so setting it twice adds a uncomfortable skip sound. - if video.watch_progress.as_secs() != 0 { - mpv.set_property( - "time-pos", - i64::try_from(video.watch_progress.as_secs()).expect("This should not overflow"), - )?; - } - Ok(()) -} - -async fn mark_video_watched(app: &App, mpv: &Mpv) -> Result<()> { - let current_video = get::currently_focused_video(app) - .await? - .expect("This should be some at this point"); - - debug!( - "playlist handler will mark video '{}' watched.", - current_video.title - ); - - save_watch_progress(app, mpv).await?; - - set::video_watched(app, ¤t_video.extractor_hash).await?; - - Ok(()) -} - -/// Saves the `watch_progress` of the currently focused video. -pub(super) async fn save_watch_progress(app: &App, mpv: &Mpv) -> Result<()> { - let current_video = get::currently_focused_video(app) - .await? - .expect("This should be some at this point"); - let watch_progress = u32::try_from( - mpv.get_property::<i64>("time-pos") - .context("Failed to get the watchprogress of the currently playling video")?, - ) - .expect("This conversion should never fail as the `time-pos` property is positive"); - - debug!( - "Setting the watch progress for the current_video '{}' to {watch_progress}s", - current_video.title_fmt_no_color() - ); - - set::video_watch_progress(app, ¤t_video.extractor_hash, watch_progress).await -} - -/// Sync the mpv playlist with the internal playlist. -/// -/// This takes an `maybe_playlist` argument, if you have already fetched the playlist and want to -/// add that. -pub(super) async fn reload_mpv_playlist( - app: &App, - mpv: &Mpv, - maybe_playlist: Option<Playlist>, - maybe_index: Option<PlaylistIndex>, -) -> Result<()> { - fn get_playlist_count(mpv: &Mpv) -> Result<usize> { - mpv.get_property::<i64>("playlist/count") - .context("Failed to get mpv playlist len") - .map(|count| { - usize::try_from(count).expect("The playlist_count should always be positive") - }) - } - - if get_playlist_count(mpv)? != 0 { - // We could also use `loadlist`, but that would require use to start a unix socket or even - // write all the video paths to a file beforehand - mpv.command("playlist-clear", &[])?; - mpv.command("playlist-remove", &["current"])?; - } - - assert_eq!( - get_playlist_count(mpv)?, - 0, - "The playlist should be empty at this point." - ); - - let playlist = if let Some(p) = maybe_playlist { - p - } else { - get::playlist(app).await? - }; - - debug!("Will add {} videos to playlist.", playlist.len()); - playlist.into_iter().try_for_each(|cache_path| { - mpv.command( - "loadfile", - &[ - cache_path.to_str().with_context(|| { - format!( - "Failed to parse the video cache path ('{}') as valid utf8", - cache_path.display() - ) - })?, - "append-play", - ], - )?; - - Ok::<(), anyhow::Error>(()) - })?; - - let index = if let Some(index) = maybe_index { - let index = usize::from(index); - let playlist_length = get_playlist_count(mpv)?; - - match index.cmp(&playlist_length) { - Ordering::Greater => { - unreachable!( - "The index '{index}' execeeds the playlist length '{playlist_length}'." - ); - } - Ordering::Less => index, - Ordering::Equal => { - // The index is pointing to the end of the playlist. We could either go the second - // to last entry (i.e., one entry back) or wrap around to the start. - // We wrap around: - 0 - } - } - } else { - get::current_playlist_index(app) - .await? - .map_or(0, usize::from) - }; - mpv.set_property("playlist-pos", index.to_string().as_str())?; - - Ok(()) -} - /// Return the status of the playback queue pub(crate) async fn status(app: &App) -> Result<Status> { let playlist = Playlist::create(app).await?; |