about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--yt/src/watch/playlist_handler/client_messages/mod.rs88
-rw-r--r--yt/src/watch/playlist_handler/mod.rs329
2 files changed, 417 insertions, 0 deletions
diff --git a/yt/src/watch/playlist_handler/client_messages/mod.rs b/yt/src/watch/playlist_handler/client_messages/mod.rs
new file mode 100644
index 0000000..4c6d9d5
--- /dev/null
+++ b/yt/src/watch/playlist_handler/client_messages/mod.rs
@@ -0,0 +1,88 @@
+use std::{env, time::Duration};
+
+use crate::{app::App, comments};
+
+use anyhow::{Context, Result, bail};
+use libmpv2::Mpv;
+use tokio::process::Command;
+
+use super::mpv_message;
+
+async fn run_self_in_external_command(app: &App, args: &[&str]) -> Result<()> {
+    let binary =
+        env::current_exe().context("Failed to determine the current executable to re-execute")?;
+
+    let status = Command::new("riverctl")
+        .args(["focus-output", "next"])
+        .status()
+        .await?;
+    if !status.success() {
+        bail!("focusing the next output failed!");
+    }
+
+    let arguments = [
+        &[
+            "--title",
+            "floating please",
+            "--command",
+            binary
+                .to_str()
+                .context("Failed to turn the executable path to a utf8-string")?,
+            "--db-path",
+            app.config
+                .paths
+                .database_path
+                .to_str()
+                .context("Failed to parse the database_path as a utf8-string")?,
+        ],
+        args,
+    ]
+    .concat();
+
+    let status = Command::new("alacritty").args(arguments).status().await?;
+    if !status.success() {
+        bail!("Falied to start `yt comments`");
+    }
+
+    let status = Command::new("riverctl")
+        .args(["focus-output", "next"])
+        .status()
+        .await?;
+
+    if !status.success() {
+        bail!("focusing the next output failed!");
+    }
+
+    Ok(())
+}
+
+pub(super) async fn handle_yt_description_external(app: &App) -> Result<()> {
+    run_self_in_external_command(app, &["description"]).await?;
+    Ok(())
+}
+pub(super) async fn handle_yt_description_local(app: &App, mpv: &Mpv) -> Result<()> {
+    let description: String = comments::description::get(app)
+        .await?
+        .chars()
+        .take(app.config.watch.local_displays_length)
+        .collect();
+
+    mpv_message(mpv, &description, Duration::from_secs(6))?;
+    Ok(())
+}
+
+pub(super) async fn handle_yt_comments_external(app: &App) -> Result<()> {
+    run_self_in_external_command(app, &["comments"]).await?;
+    Ok(())
+}
+pub(super) async fn handle_yt_comments_local(app: &App, mpv: &Mpv) -> Result<()> {
+    let comments: String = comments::get(app)
+        .await?
+        .render(false)
+        .chars()
+        .take(app.config.watch.local_displays_length)
+        .collect();
+
+    mpv_message(mpv, &comments, Duration::from_secs(6))?;
+    Ok(())
+}
diff --git a/yt/src/watch/playlist_handler/mod.rs b/yt/src/watch/playlist_handler/mod.rs
new file mode 100644
index 0000000..e6ceece
--- /dev/null
+++ b/yt/src/watch/playlist_handler/mod.rs
@@ -0,0 +1,329 @@
+use std::{cmp::Ordering, time::Duration};
+
+use crate::{
+    app::App,
+    storage::video_database::{
+        VideoStatus, VideoStatusMarker,
+        extractor_hash::ExtractorHash,
+        get::{self, Playlist, PlaylistIndex},
+        set,
+    },
+};
+
+use anyhow::{Context, Result};
+use libmpv2::{EndFileReason, Mpv, events::Event};
+use log::{debug, info};
+
+mod client_messages;
+
+#[derive(Debug, Clone, Copy)]
+pub enum Status {
+    /// There are no videos cached and no more marked to be watched.
+    /// Waiting is pointless.
+    NoMoreAvailable,
+
+    /// There are no videos cached, but some (> 0) are marked to be watched.
+    /// So we should wait for them to become available.
+    NoCached { marked_watch: usize },
+
+    /// There are videos cached and ready to be inserted into the playback queue.
+    Available { newly_available: usize },
+}
+
+fn mpv_message(mpv: &Mpv, message: &str, time: Duration) -> Result<()> {
+    mpv.command("show-text", &[
+        message,
+        time.as_millis().to_string().as_str(),
+    ])?;
+    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, &current_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, &current_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 async fn status(app: &App) -> Result<Status> {
+    let playlist = get::playlist(app).await?;
+
+    let playlist_len = playlist.len();
+    let marked_watch_num = get::videos(app, &[VideoStatusMarker::Watch]).await?.len();
+
+    if playlist_len == 0 && marked_watch_num == 0 {
+        Ok(Status::NoMoreAvailable)
+    } else if playlist_len == 0 && marked_watch_num != 0 {
+        Ok(Status::NoCached {
+            marked_watch: marked_watch_num,
+        })
+    } else if playlist_len != 0 {
+        Ok(Status::Available {
+            newly_available: playlist_len,
+        })
+    } else {
+        unreachable!(
+            "The playlist length is {playlist_len}, but the number of marked watch videos is {marked_watch_num}! This is a bug."
+        );
+    }
+}
+
+/// # Returns
+/// This will return [`true`], if the event handling should be stopped
+///
+/// # Panics
+/// Only if internal assertions fail.
+#[allow(clippy::too_many_lines)]
+pub async fn handle_mpv_event(app: &App, mpv: &Mpv, event: &Event<'_>) -> Result<bool> {
+    match event {
+        Event::EndFile(r) => match r.reason {
+            EndFileReason::Eof => {
+                info!("Mpv reached the end of the current video. Marking it watched.");
+                mark_video_watched(app, mpv).await?;
+                reload_mpv_playlist(app, mpv, None, None).await?;
+            }
+            EndFileReason::Stop => {
+                // This reason is incredibly ambiguous. It _both_ means actually pausing a
+                // video and going to the next one in the playlist.
+                // Oh, and it's also called, when a video is removed from the playlist (at
+                // least via "playlist-remove current")
+                info!("Paused video (or went to next playlist entry); Doing nothing");
+            }
+            EndFileReason::Quit => {
+                info!("Mpv quit. Exiting playback");
+
+                save_watch_progress(app, mpv).await?;
+
+                return Ok(true);
+            }
+            EndFileReason::Error => {
+                unreachable!("This should have been raised as a separate error")
+            }
+            EndFileReason::Redirect => {
+                // TODO: We probably need to handle this somehow <2025-02-17>
+            }
+        },
+        Event::StartFile(_) => {
+            let mpv_pos = usize::try_from(mpv.get_property::<i64>("playlist-pos")?)
+                .expect("The value is strictly positive");
+
+            let next_video = {
+                let yt_pos = get::current_playlist_index(app).await?.map(usize::from);
+
+                if (Some(mpv_pos) != yt_pos) || yt_pos.is_none() {
+                    let playlist = get::playlist(app).await?;
+                    let video = playlist
+                        .get(PlaylistIndex::from(mpv_pos))
+                        .expect("The mpv pos should not be out of bounds");
+
+                    set::focused(
+                        app,
+                        &video.extractor_hash,
+                        get::currently_focused_video(app)
+                            .await?
+                            .as_ref()
+                            .map(|v| &v.extractor_hash),
+                    )
+                    .await?;
+
+                    video.extractor_hash
+                } else {
+                    get::currently_focused_video(app)
+                        .await?
+                        .expect("We have a focused video")
+                        .extractor_hash
+                }
+            };
+
+            apply_video_options(app, mpv, &next_video).await?;
+        }
+        Event::Seek => {
+            save_watch_progress(app, mpv).await?;
+        }
+        Event::ClientMessage(a) => {
+            debug!("Got Client Message event: '{}'", a.join(" "));
+
+            match a.as_slice() {
+                &["yt-comments-external"] => {
+                    client_messages::handle_yt_comments_external(app).await?;
+                }
+                &["yt-comments-local"] => {
+                    client_messages::handle_yt_comments_local(app, mpv).await?;
+                }
+
+                &["yt-description-external"] => {
+                    client_messages::handle_yt_description_external(app).await?;
+                }
+                &["yt-description-local"] => {
+                    client_messages::handle_yt_description_local(app, mpv).await?;
+                }
+
+                &["yt-mark-picked"] => {
+                    let current_video = get::currently_focused_video(app)
+                        .await?
+                        .expect("This should exist at this point");
+                    let current_index = get::current_playlist_index(app)
+                        .await?
+                        .expect("This should exist, as we can mark this video picked");
+
+                    save_watch_progress(app, mpv).await?;
+
+                    set::video_status(
+                        app,
+                        &current_video.extractor_hash,
+                        VideoStatus::Pick,
+                        Some(current_video.priority),
+                    )
+                    .await?;
+
+                    reload_mpv_playlist(app, mpv, None, Some(current_index)).await?;
+                    mpv_message(mpv, "Marked the video as picked", Duration::from_secs(3))?;
+                }
+                &["yt-mark-watched"] => {
+                    let current_index = get::current_playlist_index(app)
+                        .await?
+                        .expect("This should exist, as we can mark this video picked");
+                    mark_video_watched(app, mpv).await?;
+
+                    reload_mpv_playlist(app, mpv, None, Some(current_index)).await?;
+                    mpv_message(mpv, "Marked the video watched", Duration::from_secs(3))?;
+                }
+                &["yt-check-new-videos"] => {
+                    reload_mpv_playlist(app, mpv, None, None).await?;
+                }
+                other => {
+                    debug!("Unknown message: {}", other.join(" "));
+                }
+            }
+        }
+        _ => {}
+    }
+
+    Ok(false)
+}