diff options
Diffstat (limited to 'src/watch/events.rs')
-rw-r--r-- | src/watch/events.rs | 420 |
1 files changed, 0 insertions, 420 deletions
diff --git a/src/watch/events.rs b/src/watch/events.rs deleted file mode 100644 index c1a2d13..0000000 --- a/src/watch/events.rs +++ /dev/null @@ -1,420 +0,0 @@ -// 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::{collections::HashMap, env::current_exe, time::Duration, usize}; - -use anyhow::{bail, Result}; -use libmpv2::{ - events::{Event, PlaylistEntryId}, - mpv_node::MpvNode, - EndFileReason, Mpv, -}; -use log::{debug, info, warn}; -use tokio::{process::Command, time}; - -use crate::{ - app::App, - comments::get_comments, - 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, - }, -}; - -#[derive(Debug)] -pub struct MpvEventHandler { - watch_later_block_list: HashMap<ExtractorHash, ()>, - // current_playlist: HashMap<PlaylistEntryId, ExtractorHash>, - playlist_cache: HashMap<String, ExtractorHash>, -} - -impl MpvEventHandler { - pub fn from_playlist(playlist_cache: HashMap<String, ExtractorHash>) -> Self { - Self { - // current_playlist, - playlist_cache, - watch_later_block_list: HashMap::new(), - } - } - - fn get_current_mpv_playlist( - &self, - mpv: &Mpv, - ) -> Result<HashMap<PlaylistEntryId, ExtractorHash>> { - let mpv_playlist: Vec<(String, PlaylistEntryId)> = match mpv.get_property("playlist")? { - MpvNode::ArrayIter(array) => array - .map(|val| match val { - MpvNode::MapIter(map) => { - struct BuildPlaylistEntry { - filename: Option<String>, - id: Option<PlaylistEntryId>, - } - let mut entry = BuildPlaylistEntry { - filename: None, - id: None, - }; - - map.for_each(|(key, value)| match key.as_str() { - "filename" => { - entry.filename = Some(value.str().expect("work").to_owned()) - } - "id" => { - entry.id = Some(PlaylistEntryId::new(value.i64().expect("Works"))) - } - _ => (), - }); - (entry.filename.expect("is some"), entry.id.expect("is some")) - } - _ => unreachable!(), - }) - .collect(), - _ => unreachable!(), - }; - - let mut playlist: HashMap<PlaylistEntryId, ExtractorHash> = - HashMap::with_capacity(mpv_playlist.len()); - for (path, key) in mpv_playlist { - let hash = self - .playlist_cache - .get(&path) - .expect("All path should also be stored in the cache") - .to_owned(); - playlist.insert(key, hash); - } - - // debug!("Requested the current playlist: '{:#?}'", &playlist); - - Ok(playlist) - } - - /// Checks, whether new videos are ready to be played - pub async fn possibly_add_new_videos( - &mut self, - app: &App, - mpv: &Mpv, - force_message: bool, - ) -> Result<usize> { - let play_things = get_videos(app, &[VideoStatus::Cached], Some(false)).await?; - - // There is nothing to watch - if play_things.len() == 0 { - if force_message { - Self::message(&mpv, "No new videos available to add", "3000")?; - } - return Ok(0); - } - - let mut blocked_videos = 0; - let current_playlist = self.get_current_mpv_playlist(mpv)?; - let play_things = play_things - .into_iter() - .filter(|val| { - !current_playlist - .values() - .find(|a| *a == &val.extractor_hash) - .is_some() - }) - .filter(|val| { - if self - .watch_later_block_list - .contains_key(&val.extractor_hash) - { - blocked_videos += 1; - false - } else { - true - } - }) - .collect::<Vec<_>>(); - - info!( - "{} videos are cached and will be added to the list to be played ({} are blocked)", - play_things.len(), - blocked_videos - ); - - self.playlist_cache.reserve(play_things.len()); - - let num = 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 fmt_cache_path = format!("\"{}\"", cache_path); - - let args = &[&fmt_cache_path, "append-play"]; - - mpv.execute("loadfile", args)?; - self.playlist_cache - .insert(cache_path.to_owned(), play_thing.extractor_hash); - } - - if force_message || num > 0 { - Self::message( - &mpv, - format!( - "Added {} videos ({} are marked as watch later)", - num, blocked_videos - ) - .as_str(), - "3000", - )?; - } - Ok(num) - } - - fn message(mpv: &Mpv, message: &str, time: &str) -> Result<()> { - mpv.execute("show-text", &[format!("\"{}\"", message).as_str(), time])?; - Ok(()) - } - - /// Get the hash of the currently playing video. - /// You can specify an offset, which is added to the playlist_position to get, for example, the - /// previous video (-1) or the next video (+1). - /// Beware that setting an offset can cause an property error if it's out of bound. - fn get_cvideo_hash(&self, mpv: &Mpv, offset: i64) -> Result<ExtractorHash> { - let playlist_entry_id = { - let playlist_position = { - let raw = mpv.get_property::<i64>("playlist-pos")?; - if raw == -1 { - unreachable!( "This should only be called when a current video exists. Current state: '{:#?}'", self); - } else { - (raw + offset) as usize - } - }; - - let raw = - mpv.get_property::<i64>(format!("playlist/{}/id", playlist_position).as_str())?; - PlaylistEntryId::new(raw) - }; - - // debug!("Trying to get playlist entry: '{}'", playlist_entry_id); - - let video_hash = self - .get_current_mpv_playlist(mpv)? - .remove(&playlist_entry_id) - .expect("The stored playling index should always be in the playlist"); - - Ok(video_hash) - } - async fn mark_video_watched(&self, app: &App, hash: &ExtractorHash) -> Result<()> { - let video = get_video_by_hash(app, hash).await?; - debug!("MPV handler will mark video '{}' watched.", video.title); - set_video_watched(&app, &video).await?; - Ok(()) - } - - async fn mark_video_inactive( - &mut self, - app: &App, - mpv: &Mpv, - playlist_index: PlaylistEntryId, - ) -> Result<()> { - let current_playlist = self.get_current_mpv_playlist(mpv)?; - let video_hash = current_playlist - .get(&playlist_index) - .expect("The video index should always be correctly tracked"); - - set_state_change(&app, video_hash, false).await?; - Ok(()) - } - async fn mark_video_active( - &mut self, - app: &App, - mpv: &Mpv, - playlist_index: PlaylistEntryId, - ) -> Result<()> { - let current_playlist = self.get_current_mpv_playlist(mpv)?; - let video_hash = current_playlist - .get(&playlist_index) - .expect("The video index should always be correctly tracked"); - - 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 also returns the hash of the current video - fn remove_cvideo_from_playlist(&mut self, mpv: &Mpv) -> Result<ExtractorHash> { - let hash = self.get_cvideo_hash(mpv, 0)?; - mpv.execute("playlist-remove", &["current"])?; - Ok(hash) - } - - /// Check if the playback queue is empty - pub async fn check_idle(&mut self, app: &App, mpv: &Mpv) -> Result<bool> { - if mpv.get_property::<bool>("idle-active")? { - warn!("There is nothing to watch yet. Will idle, until something is available"); - let number_of_new_videos = self.possibly_add_new_videos(app, mpv, false).await?; - - if number_of_new_videos == 0 { - time::sleep(Duration::from_secs(10)).await; - Ok(true) - } else { - Ok(false) - } - } else { - Ok(false) - } - } - - /// 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.reason { - EndFileReason::Eof => { - info!("Mpv reached eof of current video. Marking it inactive."); - - self.mark_video_inactive(app, mpv, r.playlist_entry_id) - .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); Marking it inactive"); - - self.mark_video_inactive(app, mpv, r.playlist_entry_id) - .await?; - } - EndFileReason::Quit => { - info!("Mpv quit. Exiting playback"); - - // draining the playlist is okay, as mpv is done playing - let videos = self.get_current_mpv_playlist(mpv)?; - for (_, hash) in videos { - self.mark_video_watched(app, &hash).await?; - set_state_change(&app, &hash, false).await?; - } - return Ok(true); - } - EndFileReason::Error => { - unreachable!("This will be raised as a separate error") - } - EndFileReason::Redirect => { - todo!("We probably need to handle this somehow"); - } - }, - Event::StartFile(entry_id) => { - self.possibly_add_new_videos(app, &mpv, false).await?; - - // We don't need to check, whether other videos are still active, as they should - // have been marked inactive in the `Stop` handler. - self.mark_video_active(app, mpv, entry_id).await?; - self.apply_options(app, mpv, &self.get_cvideo_hash(mpv, 0)?) - .await?; - } - 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"), - "--db-path", - app.config - .paths - .database_path - .to_str() - .expect("This should be convertible?"), - "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(app.config.watch.local_comments_length) - .collect(); - - Self::message(mpv, &comments, "6000")?; - } - &["yt-description"] => { - // let description = description(app).await?; - Self::message(&mpv, "<YT Description>", "6000")?; - } - &["yt-mark-watch-later"] => { - mpv.execute("write-watch-later-config", &[])?; - - let hash = self.remove_cvideo_from_playlist(mpv)?; - assert_eq!( - self.watch_later_block_list.insert(hash, ()), - None, - "A video should not be blocked *and* in the playlist" - ); - - Self::message(&mpv, "Marked the video to be watched later", "3000")?; - } - &["yt-mark-done-and-go-next"] => { - let cvideo_hash = self.remove_cvideo_from_playlist(mpv)?; - self.mark_video_watched(app, &cvideo_hash).await?; - - Self::message(&mpv, "Marked the video watched", "3000")?; - } - &["yt-check-new-videos"] => { - self.possibly_add_new_videos(app, mpv, true).await?; - } - other => { - debug!("Unknown message: {}", other.join(" ")) - } - } - } - _ => {} - } - - Ok(false) - } -} |