diff options
author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2024-08-24 16:38:31 +0200 |
---|---|---|
committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2024-08-24 16:38:31 +0200 |
commit | 75f2a6a9cf0bab4be6530a0f91fa05bf9d9d1b24 (patch) | |
tree | 3527b4319aefddd9d297874521f17a3e6c965f8d | |
parent | feat(watch): Idle until new videos are available instead of exiting (diff) | |
download | yt-75f2a6a9cf0bab4be6530a0f91fa05bf9d9d1b24.tar.gz yt-75f2a6a9cf0bab4be6530a0f91fa05bf9d9d1b24.zip |
refactor(watch): Don't track the playlist, use the properties of `mpv` instead
-rw-r--r-- | crates/libmpv2/src/mpv/events.rs | 30 | ||||
-rw-r--r-- | src/watch/events.rs | 271 | ||||
-rw-r--r-- | src/watch/mod.rs | 11 |
3 files changed, 237 insertions, 75 deletions
diff --git a/crates/libmpv2/src/mpv/events.rs b/crates/libmpv2/src/mpv/events.rs index cbe1ef3..6fb4683 100644 --- a/crates/libmpv2/src/mpv/events.rs +++ b/crates/libmpv2/src/mpv/events.rs @@ -41,6 +41,20 @@ pub mod mpv_event_id { pub use libmpv2_sys::mpv_event_id_MPV_EVENT_VIDEO_RECONFIG as VideoReconfig; } +/// A unique id of every entry MPV has loaded +#[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Hash)] +pub struct PlaylistEntryId(i64); +impl PlaylistEntryId { + pub fn new(val: i64) -> Self { + Self(val) + } +} +impl Display for PlaylistEntryId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + #[derive(Debug)] /// Data that is returned by both `GetPropertyReply` and `PropertyChange` events. pub enum PropertyData<'a> { @@ -80,7 +94,11 @@ impl<'a> PropertyData<'a> { } } -pub type PlaylistEntryId = i64; +#[derive(Debug)] +pub struct EndFileEvent { + pub reason: EndFileReason, + pub playlist_entry_id: PlaylistEntryId, +} #[derive(Debug)] pub enum Event<'a> { @@ -106,7 +124,7 @@ pub enum Event<'a> { /// Event received when a new file is playing StartFile(PlaylistEntryId), /// Event received when the file being played currently has stopped, for an error or not - EndFile(EndFileReason), + EndFile(EndFileEvent), /// Event received when a file has been *loaded*, but has not been started FileLoaded, ClientMessage(Vec<&'a str>), @@ -270,7 +288,7 @@ impl EventContext { mpv_event_id::StartFile => { let playlist_id = unsafe { *(event.data as *mut i64) }; - Some(Ok(Event::StartFile(playlist_id))) + Some(Ok(Event::StartFile(PlaylistEntryId(playlist_id)))) } mpv_event_id::EndFile => { let end_file = unsafe { *(event.data as *mut libmpv2_sys::mpv_event_end_file) }; @@ -280,7 +298,11 @@ impl EventContext { if let Err(e) = mpv_err((), end_file.error) { Some(Err(e)) } else { - Some(Ok(Event::EndFile(end_file.reason.into()))) + let event = EndFileEvent { + reason: end_file.reason.into(), + playlist_entry_id: PlaylistEntryId(end_file.playlist_entry_id), + }; + Some(Ok(Event::EndFile(event))) } } mpv_event_id::FileLoaded => Some(Ok(Event::FileLoaded)), diff --git a/src/watch/events.rs b/src/watch/events.rs index 0873bc2..c1a2d13 100644 --- a/src/watch/events.rs +++ b/src/watch/events.rs @@ -8,11 +8,15 @@ // 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, time::Duration, usize}; +use std::{collections::HashMap, env::current_exe, time::Duration, usize}; use anyhow::{bail, Result}; -use libmpv2::{events::Event, EndFileReason, Mpv}; -use log::{debug, error, info, warn}; +use libmpv2::{ + events::{Event, PlaylistEntryId}, + mpv_node::MpvNode, + EndFileReason, Mpv, +}; +use log::{debug, info, warn}; use tokio::{process::Command, time}; use crate::{ @@ -26,21 +30,72 @@ use crate::{ }, }; +#[derive(Debug)] pub struct MpvEventHandler { - currently_playing_index: Option<usize>, - current_playlist_position: usize, - current_playlist: Vec<ExtractorHash>, + watch_later_block_list: HashMap<ExtractorHash, ()>, + // current_playlist: HashMap<PlaylistEntryId, ExtractorHash>, + playlist_cache: HashMap<String, ExtractorHash>, } impl MpvEventHandler { - pub fn from_playlist(playlist: Vec<ExtractorHash>) -> Self { + pub fn from_playlist(playlist_cache: HashMap<String, ExtractorHash>) -> Self { Self { - currently_playing_index: None, - current_playlist: playlist, - current_playlist_position: 0, + // 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, @@ -58,17 +113,36 @@ impl MpvEventHandler { 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| !self.current_playlist.contains(&val.extractor_hash)) + .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", - play_things.len() + "{} videos are cached and will be added to the list to be played ({} are blocked)", + play_things.len(), + blocked_videos ); - self.current_playlist.reserve(play_things.len()); + self.playlist_cache.reserve(play_things.len()); let num = play_things.len(); for play_thing in play_things { @@ -76,16 +150,25 @@ impl MpvEventHandler { 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 fmt_cache_path = format!("\"{}\"", cache_path); - let args = &[&cache_path, "append-play"]; + let args = &[&fmt_cache_path, "append-play"]; mpv.execute("loadfile", args)?; - self.current_playlist.push(play_thing.extractor_hash); + self.playlist_cache + .insert(cache_path.to_owned(), play_thing.extractor_hash); } if force_message || num > 0 { - Self::message(&mpv, format!("Added {} videos", num).as_str(), "3000")?; + Self::message( + &mpv, + format!( + "Added {} videos ({} are marked as watch later)", + num, blocked_videos + ) + .as_str(), + "3000", + )?; } Ok(num) } @@ -95,33 +178,67 @@ impl MpvEventHandler { Ok(()) } - async fn mark_video_watched(&mut self, app: &App, hash: &ExtractorHash) -> Result<()> { + /// 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_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?; - } - error!("Expected a current video, but found none (while trying to mark it watched)"); - 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?; - } - error!("Expected a current video, but found none (while trying to mark it inactive)"); + 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, playlist_index: usize) -> Result<()> { - let video_hash = &self.current_playlist[(playlist_index) as usize]; - self.currently_playing_index = Some(playlist_index); + 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(()) } @@ -134,14 +251,25 @@ impl MpvEventHandler { 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 self.current_playlist.is_empty() { + if mpv.get_property::<bool>("idle-active")? { warn!("There is nothing to watch yet. Will idle, until something is available"); - self.possibly_add_new_videos(app, mpv, false).await?; + let number_of_new_videos = self.possibly_add_new_videos(app, mpv, false).await?; - time::sleep(Duration::from_secs(10)).await; - Ok(true) + if number_of_new_videos == 0 { + time::sleep(Duration::from_secs(10)).await; + Ok(true) + } else { + Ok(false) + } } else { Ok(false) } @@ -155,27 +283,31 @@ impl MpvEventHandler { event: Event<'a>, ) -> Result<bool> { match event { - Event::EndFile(r) => match r { + Event::EndFile(r) => match r.reason { EndFileReason::Eof => { info!("Mpv reached eof of current video. Marking it inactive."); - self.mark_cvideo_inactive(app).await?; + self.mark_video_inactive(app, mpv, r.playlist_entry_id) + .await?; } EndFileReason::Stop => { - info!("Mpv stopped current video. Marking it inactive."); - - // TODO: Should we also mark the video watched? <2024-08-21> - - self.mark_cvideo_inactive(app).await?; + // 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 = mem::take(&mut self.current_playlist); - for video in videos { - self.mark_video_watched(app, &video).await?; - set_state_change(&app, &video, false).await?; + 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); } @@ -186,20 +318,15 @@ impl MpvEventHandler { todo!("We probably need to handle this somehow"); } }, - Event::StartFile(playlist_index) => { + Event::StartFile(entry_id) => { self.possibly_add_new_videos(app, &mpv, false).await?; - self.mark_video_active(app, (playlist_index - 1) as usize) + // 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?; - 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(" ")); @@ -221,6 +348,12 @@ impl MpvEventHandler { "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() @@ -254,17 +387,21 @@ impl MpvEventHandler { Self::message(&mpv, "<YT Description>", "6000")?; } &["yt-mark-watch-later"] => { - self.mark_cvideo_inactive(app).await?; mpv.execute("write-watch-later-config", &[])?; - mpv.execute("playlist-remove", &["current"])?; + + 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"] => { - self.mark_cvideo_watched(app).await?; - self.mark_cvideo_inactive(app).await?; + let cvideo_hash = self.remove_cvideo_from_playlist(mpv)?; + self.mark_video_watched(app, &cvideo_hash).await?; - mpv.execute("playlist-remove", &["current"])?; Self::message(&mpv, "Marked the video watched", "3000")?; } &["yt-check-new-videos"] => { diff --git a/src/watch/mod.rs b/src/watch/mod.rs index 9eb1c18..376b245 100644 --- a/src/watch/mod.rs +++ b/src/watch/mod.rs @@ -8,6 +8,8 @@ // 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}; @@ -77,20 +79,21 @@ pub async fn watch(app: &App) -> Result<()> { play_things.len() ); - let mut playlist_cache: Vec<ExtractorHash> = Vec::with_capacity(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 cache_path = format!("\"{}\"", cache_path); + let fmt_cache_path = format!("\"{}\"", cache_path); - let args = &[&cache_path, "append-play"]; + let args = &[&fmt_cache_path, "append-play"]; mpv.execute("loadfile", args)?; - playlist_cache.push(play_thing.extractor_hash); + playlist_cache.insert(cache_path.to_owned(), play_thing.extractor_hash); } let mut mpv_event_handler = MpvEventHandler::from_playlist(playlist_cache); |