// 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::{env::current_exe, mem, usize}; use anyhow::{bail, Result}; use libmpv2::{events::Event, EndFileReason, Mpv}; use log::{debug, info}; use tokio::process::Command; 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, }, }; pub struct MpvEventHandler { currently_playing_index: Option, current_playlist_position: usize, current_playlist: Vec, } impl MpvEventHandler { pub fn from_playlist(playlist: Vec) -> Self { Self { currently_playing_index: None, current_playlist: playlist, current_playlist_position: 0, } } /// Checks, whether new videos are ready to be played pub async fn possibly_add_new_videos(&mut self, app: &App, mpv: &Mpv) -> Result<()> { let play_things = get_videos(app, &[VideoStatus::Cached], Some(false)).await?; // There is nothing to watch if play_things.len() == 0 { return Ok(()); } let play_things = play_things .into_iter() .filter(|val| !self.current_playlist.contains(&val.extractor_hash)) .collect::>(); info!( "{} videos are cached and will be added to the list to be played", play_things.len() ); self.current_playlist.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 cache_path = format!("\"{}\"", cache_path); let args = &[&cache_path, "append-play"]; mpv.execute("loadfile", args)?; self.current_playlist.push(play_thing.extractor_hash); } Ok(()) } async fn mark_video_watched(&mut self, app: &App, hash: &ExtractorHash) -> Result<()> { let video = get_video_by_hash(app, hash).await?; 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?; } 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?; } 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); 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 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 { EndFileReason::Eof => { info!("Mpv reached eof of current video. Marking it inactive."); self.mark_cvideo_inactive(app).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?; } 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?; } return Ok(true); } EndFileReason::Error => { unreachable!("This have raised a separate error") } EndFileReason::Redirect => { todo!("We probably need to handle this somehow"); } }, Event::StartFile(playlist_index) => { self.possibly_add_new_videos(app, &mpv).await?; self.mark_video_active(app, (playlist_index - 1) as usize) .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(" ")); 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"), "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(); mpv.execute("show-text", &[&format!("'{}'", comments), "6000"])?; } &["yt-description"] => { // let description = description(app).await?; mpv.execute("script-message", &["osc-message", "''"])?; } &["yt-mark-watch-later"] => { self.mark_cvideo_inactive(app).await?; mpv.execute("write-watch-later-config", &[])?; mpv.execute("playlist-remove", &["current"])?; } other => { debug!("Unknown message: {}", other.join(" ")) } } } _ => {} } Ok(false) } }