// 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::{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,
constants::LOCAL_COMMENTS_LENGTH,
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<usize>,
current_playlist_position: usize,
current_playlist: Vec<ExtractorHash>,
}
impl MpvEventHandler {
pub fn from_playlist(playlist: Vec<ExtractorHash>) -> 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::<Vec<_>>();
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=<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<bool> {
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(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 Description>'"])?;
}
&["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)
}
}