From 83643e0370b101968bd3de5e9a81c2b309955cbd Mon Sep 17 00:00:00 2001 From: Benedikt Peetz Date: Sun, 25 Aug 2024 15:53:05 +0200 Subject: refactor(watch/playlist_handler): Init This facilitates outsourcing the mpv playlist operations and overlaying them with an cache that provides the facility to convert for `playlist_entry_id`s to `ExtractorHash`es even after their corresponding video has been removed from the playlist. --- src/watch/events.rs | 420 ----------------------------------- src/watch/events/mod.rs | 374 +++++++++++++++++++++++++++++++ src/watch/events/playlist_handler.rs | 96 ++++++++ 3 files changed, 470 insertions(+), 420 deletions(-) delete mode 100644 src/watch/events.rs create mode 100644 src/watch/events/mod.rs create mode 100644 src/watch/events/playlist_handler.rs (limited to 'src/watch') 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 -// 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 . - -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, - // current_playlist: HashMap, - playlist_cache: HashMap, -} - -impl MpvEventHandler { - pub fn from_playlist(playlist_cache: HashMap) -> Self { - Self { - // current_playlist, - playlist_cache, - watch_later_block_list: HashMap::new(), - } - } - - fn get_current_mpv_playlist( - &self, - mpv: &Mpv, - ) -> Result> { - 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, - id: Option, - } - 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 = - 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 { - 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::>(); - - 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 { - let playlist_entry_id = { - let playlist_position = { - let raw = mpv.get_property::("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::(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=` - 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 { - 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 { - if mpv.get_property::("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 { - 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, "", "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/mod.rs b/src/watch/events/mod.rs new file mode 100644 index 0000000..9ca12fd --- /dev/null +++ b/src/watch/events/mod.rs @@ -0,0 +1,374 @@ +// yt - A fully featured command line YouTube client +// +// Copyright (C) 2024 Benedikt Peetz +// 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 . + +use std::{collections::HashMap, env::current_exe, mem, time::Duration, usize}; + +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, + playlist_handler: PlaylistHandler, +} + +impl MpvEventHandler { + pub fn from_playlist(playlist_cache: HashMap) -> 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 { + 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.playlist_handler.playlist_ids(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::>(); + + 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 { + let playlist_entry_id = { + let playlist_position = { + let raw = mpv.get_property::("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::(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=` + 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 { + 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 { + if mpv.get_property::("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 { + 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 { + 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, "", "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 new file mode 100644 index 0000000..8f2f322 --- /dev/null +++ b/src/watch/events/playlist_handler.rs @@ -0,0 +1,96 @@ +// yt - A fully featured command line YouTube client +// +// Copyright (C) 2024 Benedikt Peetz +// 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 . + +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, + + /// A map of the playlist_entry_id field to their corresponding extractor hashes. + playlist_ids: HashMap, +} +impl PlaylistHandler { + pub fn from_cache(cache: HashMap) -> 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> { + 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, + id: Option, + } + 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 = + 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 { + if !self.playlist_ids.contains_key(&id) { + self.playlist_ids.insert(id, hash); + } + } + + Ok(&self.playlist_ids) + } +} -- cgit 1.4.1