about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-08-24 16:38:31 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-08-24 16:38:31 +0200
commit75f2a6a9cf0bab4be6530a0f91fa05bf9d9d1b24 (patch)
tree3527b4319aefddd9d297874521f17a3e6c965f8d
parentfeat(watch): Idle until new videos are available instead of exiting (diff)
downloadyt-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.rs30
-rw-r--r--src/watch/events.rs271
-rw-r--r--src/watch/mod.rs11
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);