about summary refs log tree commit diff stats
path: root/src/watch/events.rs
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-08-21 10:49:23 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-08-21 11:28:43 +0200
commit1debeb77f7986de1b659dcfdc442de6415e1d9f5 (patch)
tree4df3e7c3f6a2d1ec116e4088c5ace7f143a8b05f /src/watch/events.rs
downloadyt-1debeb77f7986de1b659dcfdc442de6415e1d9f5.tar.gz
yt-1debeb77f7986de1b659dcfdc442de6415e1d9f5.zip
chore: Initial Commit
This repository was migrated out of my nixos-config.
Diffstat (limited to 'src/watch/events.rs')
-rw-r--r--src/watch/events.rs235
1 files changed, 235 insertions, 0 deletions
diff --git a/src/watch/events.rs b/src/watch/events.rs
new file mode 100644
index 0000000..815ad28
--- /dev/null
+++ b/src/watch/events.rs
@@ -0,0 +1,235 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::{env::current_exe, mem, usize};
+
+use anyhow::{bail, Result};
+use libmpv2::{events::Event, EndFileReason, Mpv};
+use log::{debug, info};
+use tokio::process::Command;
+
+use crate::{
+    app::App,
+    comments::get_comments,
+    constants::LOCAL_COMMENTS_LENGTH,
+    storage::video_database::{
+        extractor_hash::ExtractorHash,
+        getters::{get_video_by_hash, get_video_mpv_opts, get_videos},
+        setters::{set_state_change, set_video_watched},
+        VideoStatus,
+    },
+};
+
+pub struct MpvEventHandler {
+    currently_playing_index: Option<usize>,
+    current_playlist_position: usize,
+    current_playlist: Vec<ExtractorHash>,
+}
+
+impl MpvEventHandler {
+    pub fn from_playlist(playlist: Vec<ExtractorHash>) -> Self {
+        Self {
+            currently_playing_index: None,
+            current_playlist: playlist,
+            current_playlist_position: 0,
+        }
+    }
+
+    /// Checks, whether new videos are ready to be played
+    pub async fn possibly_add_new_videos(&mut self, app: &App, mpv: &Mpv) -> Result<()> {
+        let play_things = get_videos(app, &[VideoStatus::Cached], Some(false)).await?;
+
+        // There is nothing to watch
+        if play_things.len() == 0 {
+            return Ok(());
+        }
+
+        let play_things = play_things
+            .into_iter()
+            .filter(|val| !self.current_playlist.contains(&val.extractor_hash))
+            .collect::<Vec<_>>();
+
+        info!(
+            "{} videos are cached and will be added to the list to be played",
+            play_things.len()
+        );
+
+        self.current_playlist.reserve(play_things.len());
+
+        for play_thing in play_things {
+            debug!("Adding '{}' to playlist.", play_thing.title);
+
+            let orig_cache_path = play_thing.cache_path.expect("Is cached and thus some");
+            let cache_path = orig_cache_path.to_str().expect("Should be vaild utf8");
+            let cache_path = format!("\"{}\"", cache_path);
+
+            let args = &[&cache_path, "append-play"];
+
+            mpv.execute("loadfile", args)?;
+            self.current_playlist.push(play_thing.extractor_hash);
+        }
+
+        Ok(())
+    }
+
+    async fn mark_video_watched(&mut self, app: &App, hash: &ExtractorHash) -> Result<()> {
+        let video = get_video_by_hash(app, hash).await?;
+        set_video_watched(&app, &video).await?;
+        Ok(())
+    }
+    async fn mark_cvideo_watched(&mut self, app: &App) -> Result<()> {
+        if let Some(index) = self.currently_playing_index {
+            let video_hash = self.current_playlist[(index) as usize].clone();
+            self.mark_video_watched(app, &video_hash).await?;
+        }
+        Ok(())
+    }
+
+    async fn mark_cvideo_inactive(&mut self, app: &App) -> Result<()> {
+        if let Some(index) = self.currently_playing_index {
+            let video_hash = &self.current_playlist[(index) as usize];
+            self.currently_playing_index = None;
+            set_state_change(&app, video_hash, false).await?;
+        }
+        Ok(())
+    }
+    async fn mark_video_active(&mut self, app: &App, playlist_index: usize) -> Result<()> {
+        let video_hash = &self.current_playlist[(playlist_index) as usize];
+        self.currently_playing_index = Some(playlist_index);
+        set_state_change(&app, video_hash, true).await?;
+        Ok(())
+    }
+
+    /// Apply the options set with e.g. `watch --speed=<speed>`
+    async fn apply_options(&self, app: &App, mpv: &Mpv, hash: &ExtractorHash) -> Result<()> {
+        let options = get_video_mpv_opts(app, hash).await?;
+
+        mpv.set_property("speed", options.playback_speed)?;
+
+        Ok(())
+    }
+
+    /// This will return [`true`], if the event handling should be stopped
+    pub async fn handle_mpv_event<'a>(
+        &mut self,
+        app: &App,
+        mpv: &Mpv,
+        event: Event<'a>,
+    ) -> Result<bool> {
+        match event {
+            Event::EndFile(r) => match r {
+                EndFileReason::Eof => {
+                    info!("Mpv reached eof of current video. Marking it watched.");
+
+                    self.mark_cvideo_watched(app).await?;
+                    self.mark_cvideo_inactive(app).await?;
+                }
+                EndFileReason::Stop => {}
+                EndFileReason::Quit => {
+                    info!("Mpv quit. Exiting playback");
+
+                    // draining the playlist is okay, as mpv is done playing
+                    let videos = mem::take(&mut self.current_playlist);
+                    for video in videos {
+                        self.mark_video_watched(app, &video).await?;
+                        set_state_change(&app, &video, false).await?;
+                    }
+                    return Ok(true);
+                }
+                EndFileReason::Error => {
+                    unreachable!("This have raised a separate error")
+                }
+                EndFileReason::Redirect => {
+                    todo!("We probably need to handle this somehow");
+                }
+            },
+            Event::StartFile(playlist_index) => {
+                self.possibly_add_new_videos(app, &mpv).await?;
+
+                self.mark_video_active(app, (playlist_index - 1) as usize)
+                    .await?;
+                self.current_playlist_position = (playlist_index - 1) as usize;
+                self.apply_options(
+                    app,
+                    mpv,
+                    &self.current_playlist[self.current_playlist_position],
+                )
+                .await?;
+            }
+            Event::FileLoaded => {}
+            Event::ClientMessage(a) => {
+                debug!("Got Client Message event: '{}'", a.join(" "));
+
+                match a.as_slice() {
+                    &["yt-comments-external"] => {
+                        let binary = current_exe().expect("A current exe should exist");
+
+                        let status = Command::new("riverctl")
+                            .args(["focus-output", "next"])
+                            .status()
+                            .await?;
+                        if !status.success() {
+                            bail!("focusing the next output failed!");
+                        }
+
+                        let status = Command::new("alacritty")
+                            .args(&[
+                                "--title",
+                                "floating please",
+                                "--command",
+                                binary.to_str().expect("Should be valid unicode"),
+                                "comments",
+                            ])
+                            .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!");
+                        }
+                    }
+                    &["yt-comments-local"] => {
+                        let comments: String = get_comments(app)
+                            .await?
+                            .render(false)
+                            .replace("\"", "")
+                            .replace("'", "")
+                            .chars()
+                            .take(LOCAL_COMMENTS_LENGTH)
+                            .collect();
+
+                        mpv.execute("show-text", &[&format!("'{}'", comments), "6000"])?;
+                    }
+                    &["yt-description"] => {
+                        // let description = description(app).await?;
+                        mpv.execute("script-message", &["osc-message", "'<YT Description>'"])?;
+                    }
+                    &["yt-mark-watch-later"] => {
+                        self.mark_cvideo_inactive(app).await?;
+                        mpv.execute("write-watch-later-config", &[])?;
+                        mpv.execute("playlist-remove", &["current"])?;
+                    }
+                    other => {
+                        debug!("Unknown message: {}", other.join(" "))
+                    }
+                }
+            }
+            _ => {}
+        }
+
+        Ok(false)
+    }
+}