diff options
Diffstat (limited to 'src/watch')
-rw-r--r-- | src/watch/events.rs | 235 | ||||
-rw-r--r-- | src/watch/mod.rs | 118 |
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(()) +} |