aboutsummaryrefslogtreecommitdiffstats
path: root/src/watch
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
downloadyt-1debeb77f7986de1b659dcfdc442de6415e1d9f5.zip
chore: Initial Commit
This repository was migrated out of my nixos-config.
Diffstat (limited to 'src/watch')
-rw-r--r--src/watch/events.rs235
-rw-r--r--src/watch/mod.rs118
2 files changed, 353 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)
+ }
+}
diff --git a/src/watch/mod.rs b/src/watch/mod.rs
new file mode 100644
index 0000000..374c1d7
--- /dev/null
+++ b/src/watch/mod.rs
@@ -0,0 +1,118 @@
+// 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 anyhow::Result;
+use events::MpvEventHandler;
+use libmpv2::{events::EventContext, Mpv};
+use log::{debug, info, warn};
+
+use crate::{
+ app::App,
+ cache::maintain,
+ constants::{mpv_config_path, mpv_input_path},
+ storage::video_database::{extractor_hash::ExtractorHash, getters::get_videos, VideoStatus},
+};
+
+pub mod events;
+
+pub async fn watch(app: &App) -> Result<()> {
+ maintain(app, false).await?;
+
+ // 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")?;
+
+ // 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")?;
+ Ok(())
+ })?;
+
+ let config_path = mpv_config_path()?;
+ if config_path.try_exists()? {
+ info!("Found mpv.conf at '{}'!", config_path.display());
+ mpv.execute(
+ "load-config-file",
+ &[config_path.to_str().expect("This should be utf8-able")],
+ )?;
+ } else {
+ warn!(
+ "Did not find a mpv.conf file at '{}'",
+ config_path.display()
+ );
+ }
+
+ let input_path = mpv_input_path()?;
+ if input_path.try_exists()? {
+ info!("Found mpv.input.conf at '{}'!", input_path.display());
+ mpv.execute(
+ "load-input-conf",
+ &[input_path.to_str().expect("This should be utf8-able")],
+ )?;
+ } else {
+ warn!(
+ "Did not find a mpv.input.conf file at '{}'",
+ input_path.display()
+ );
+ }
+
+ let mut ev_ctx = EventContext::new(mpv.ctx);
+ ev_ctx.disable_deprecated_events()?;
+
+ let play_things = get_videos(app, &[VideoStatus::Cached], Some(false)).await?;
+ info!(
+ "{} videos are cached and ready to be played",
+ play_things.len()
+ );
+
+ // There is nothing to watch
+ if play_things.len() == 0 {
+ return Ok(());
+ }
+
+ let mut playlist_cache: Vec<ExtractorHash> = Vec::with_capacity(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)?;
+
+ playlist_cache.push(play_thing.extractor_hash);
+ }
+
+ let mut mpv_event_handler = MpvEventHandler::from_playlist(playlist_cache);
+ loop {
+ if let Some(ev) = ev_ctx.wait_event(600.) {
+ match ev {
+ Ok(event) => {
+ debug!("Mpv event triggered: {:#?}", event);
+ if mpv_event_handler.handle_mpv_event(app, &mpv, event).await? {
+ break;
+ }
+ }
+ Err(e) => debug!("Mpv Event errored: {}", e),
+ }
+ }
+ }
+
+ Ok(())
+}