diff options
Diffstat (limited to 'src/watch')
-rw-r--r-- | src/watch/events/mod.rs | 369 | ||||
-rw-r--r-- | src/watch/events/playlist_handler.rs | 94 | ||||
-rw-r--r-- | src/watch/mod.rs | 117 |
3 files changed, 0 insertions, 580 deletions
diff --git a/src/watch/events/mod.rs b/src/watch/events/mod.rs deleted file mode 100644 index 41a7772..0000000 --- a/src/watch/events/mod.rs +++ /dev/null @@ -1,369 +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, mem, time::Duration}; - -use anyhow::{bail, Result}; -use libmpv2::{ - events::{Event, PlaylistEntryId}, - 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, - }, -}; - -use playlist_handler::PlaylistHandler; - -mod playlist_handler; - -#[derive(Debug)] -pub struct MpvEventHandler { - watch_later_block_list: HashMap<ExtractorHash, ()>, - playlist_handler: PlaylistHandler, -} - -impl MpvEventHandler { - pub fn from_playlist(playlist_cache: HashMap<String, ExtractorHash>) -> Self { - let playlist_handler = PlaylistHandler::from_cache(playlist_cache); - Self { - playlist_handler, - watch_later_block_list: HashMap::new(), - } - } - - /// 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.is_empty() { - 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.playlist_handler.playlist_ids(mpv)?; - let play_things = play_things - .into_iter() - .filter(|val| !current_playlist.values().any(|a| a == &val.extractor_hash)) - .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 - ); - - let num = play_things.len(); - self.playlist_handler.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 fmt_cache_path = format!("\"{}\"", cache_path); - - let args = &[&fmt_cache_path, "append-play"]; - - mpv.execute("loadfile", args)?; - self.playlist_handler - .add(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(&mut 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 - .playlist_handler - .playlist_ids(mpv)? - .get(&playlist_entry_id) - .expect("The stored playling index should always be in the playlist") - .to_owned(); - - 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.playlist_handler.playlist_ids(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.playlist_handler.playlist_ids(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 mut handler = mem::take(&mut self.playlist_handler); - let videos = handler.playlist_ids(mpv)?; - for hash in videos.values() { - 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?; - let hash = self.get_cvideo_hash(mpv, 0)?; - self.apply_options(app, mpv, &hash).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) - } -} diff --git a/src/watch/events/playlist_handler.rs b/src/watch/events/playlist_handler.rs deleted file mode 100644 index 0933856..0000000 --- a/src/watch/events/playlist_handler.rs +++ /dev/null @@ -1,94 +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; - -use anyhow::Result; -use libmpv2::{events::PlaylistEntryId, mpv_node::MpvNode, Mpv}; - -use crate::storage::video_database::extractor_hash::ExtractorHash; - -#[derive(Debug, Default)] -pub struct PlaylistHandler { - /// A map of the original file paths to the videos extractor hashes. - /// Used to get the extractor hash from a video returned by mpv - playlist_cache: HashMap<String, ExtractorHash>, - - /// A map of the playlist_entry_id field to their corresponding extractor hashes. - playlist_ids: HashMap<PlaylistEntryId, ExtractorHash>, -} -impl PlaylistHandler { - pub fn from_cache(cache: HashMap<String, ExtractorHash>) -> Self { - Self { - playlist_cache: cache, - playlist_ids: HashMap::new(), - } - } - - pub fn reserve(&mut self, len: usize) { - self.playlist_cache.reserve(len) - } - pub fn add(&mut self, cache_path: String, extractor_hash: ExtractorHash) { - assert_eq!( - self.playlist_cache.insert(cache_path, extractor_hash), - None, - "Only new video should ever be added" - ); - } - - pub fn playlist_ids(&mut 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); - } - - for (id, hash) in playlist { - self.playlist_ids.entry(id).or_insert(hash); - } - - Ok(&self.playlist_ids) - } -} diff --git a/src/watch/mod.rs b/src/watch/mod.rs deleted file mode 100644 index 3bcf1fc..0000000 --- a/src/watch/mod.rs +++ /dev/null @@ -1,117 +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; - -use anyhow::Result; -use events::MpvEventHandler; -use libmpv2::{events::EventContext, Mpv}; -use log::{debug, info, warn}; - -use crate::{ - app::App, - cache::maintain, - 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 = &app.config.paths.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 = &app.config.paths.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() - ); - - let mut playlist_cache: HashMap<String, ExtractorHash> = - HashMap::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 fmt_cache_path = format!("\"{}\"", cache_path); - - let args = &[&fmt_cache_path, "append-play"]; - - mpv.execute("loadfile", args)?; - - playlist_cache.insert(cache_path.to_owned(), play_thing.extractor_hash); - } - - let mut mpv_event_handler = MpvEventHandler::from_playlist(playlist_cache); - loop { - while mpv_event_handler.check_idle(app, &mpv).await? {} - - 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(()) -} |