From dbfabfc078dea5d1ec30cd3e7ae94f76a8065116 Mon Sep 17 00:00:00 2001 From: Benedikt Peetz Date: Tue, 20 Aug 2024 15:37:41 +0200 Subject: feat(pkgs/yt): Implement additional features and bug fixes --- pkgs/by-name/yt/yt/scripts/mkdb.sh | 1 + pkgs/by-name/yt/yt/src/cache/mod.rs | 26 ++++---- pkgs/by-name/yt/yt/src/cli.rs | 40 ++++++++++--- pkgs/by-name/yt/yt/src/constants.rs | 4 ++ .../by-name/yt/yt/src/download/download_options.rs | 21 +++++-- pkgs/by-name/yt/yt/src/download/mod.rs | 35 ++--------- pkgs/by-name/yt/yt/src/main.rs | 22 +++---- pkgs/by-name/yt/yt/src/select/cmds.rs | 40 ++++++++++--- pkgs/by-name/yt/yt/src/select/mod.rs | 70 ++++++++++++++-------- .../yt/yt/src/select/selection_file/display.rs | 11 +++- .../yt/yt/src/storage/video_database/downloader.rs | 2 +- .../yt/yt/src/storage/video_database/getters.rs | 63 ++++++++++++++++++- .../yt/yt/src/storage/video_database/mod.rs | 49 ++++++++++++++- .../yt/yt/src/storage/video_database/schema.sql | 9 +++ .../yt/yt/src/storage/video_database/setters.rs | 58 +++++++++++++++--- pkgs/by-name/yt/yt/src/watch/events.rs | 33 ++++++++-- pkgs/by-name/yt/yt/src/watch/mod.rs | 2 + pkgs/by-name/yt/yt/yt_dlp/src/wrapper/info_json.rs | 2 + 18 files changed, 370 insertions(+), 118 deletions(-) (limited to 'pkgs/by-name') diff --git a/pkgs/by-name/yt/yt/scripts/mkdb.sh b/pkgs/by-name/yt/yt/scripts/mkdb.sh index f4246a49..3ae637d9 100755 --- a/pkgs/by-name/yt/yt/scripts/mkdb.sh +++ b/pkgs/by-name/yt/yt/scripts/mkdb.sh @@ -4,6 +4,7 @@ root="$(dirname "$0")/.." db="$root/target/database.sqlite" [ -f "$db" ] && rm "$db" +[ -d "$root/target" ] || mkdir "$root/target" sqlite3 "$db" <"$root/src/storage/video_database/schema.sql" diff --git a/pkgs/by-name/yt/yt/src/cache/mod.rs b/pkgs/by-name/yt/yt/src/cache/mod.rs index f9715e40..87f42604 100644 --- a/pkgs/by-name/yt/yt/src/cache/mod.rs +++ b/pkgs/by-name/yt/yt/src/cache/mod.rs @@ -1,7 +1,6 @@ -use std::path::Path; - use anyhow::Result; use log::info; +use tokio::fs; use crate::{ app::App, @@ -10,21 +9,28 @@ use crate::{ }, }; -async fn invalidate_video(app: &App, video: &Video) -> Result<()> { +async fn invalidate_video(app: &App, video: &Video, hard: bool) -> Result<()> { info!("Invalidating cache of video: '{}'", video.title); + if hard { + if let Some(path) = &video.cache_path { + info!("Removing cached video at: '{}'", path.display()); + fs::remove_file(path).await?; + } + } + set_video_cache_path(app, &video.extractor_hash, None).await?; Ok(()) } -pub async fn invalidate(app: &App) -> Result<()> { +pub async fn invalidate(app: &App, hard: bool) -> Result<()> { let all_cached_things = get_videos(app, &[VideoStatus::Cached]).await?; info!("Got videos to invalidate: '{}'", all_cached_things.len()); for video in all_cached_things { - invalidate_video(app, &video).await? + invalidate_video(app, &video, hard).await? } Ok(()) @@ -49,11 +55,11 @@ pub async fn maintain(app: &App, all: bool) -> Result<()> { let cached_videos = get_videos(app, domain.as_slice()).await?; for vid in cached_videos { - let path: &Path = vid.cache_path.as_ref().expect("This is some"); - - info!("Checking if path ('{}') exists", path.display()); - if !path.exists() { - invalidate_video(app, &vid).await?; + if let Some(path) = vid.cache_path.as_ref() { + info!("Checking if path ('{}') exists", path.display()); + if !path.exists() { + invalidate_video(app, &vid, false).await?; + } } } diff --git a/pkgs/by-name/yt/yt/src/cli.rs b/pkgs/by-name/yt/yt/src/cli.rs index 799c9ee4..932712cc 100644 --- a/pkgs/by-name/yt/yt/src/cli.rs +++ b/pkgs/by-name/yt/yt/src/cli.rs @@ -5,7 +5,7 @@ use clap::{ArgAction, Args, Parser, Subcommand}; use url::Url; use crate::{ - select::selection_file::duration::Duration, + constants, select::selection_file::duration::Duration, storage::video_database::extractor_hash::LazyExtractorHash, }; @@ -30,8 +30,9 @@ pub struct CliArgs { pub enum Command { /// Download and cache URLs Download { - /// The list of URLs to download (empty: use the database) - urls: Vec, + /// Forcefully re-download all cached videos (i.e. delete the cache path, then download). + #[arg(short, long)] + force: bool, }, /// Watch the already cached (and selected) videos @@ -134,20 +135,31 @@ pub struct SharedSelectionCommandArgs { pub url: Url, } -#[derive(Subcommand, Clone, Debug, Default)] +#[derive(Subcommand, Clone, Debug)] #[command(infer_subcommands = true)] pub enum SelectCommand { - #[default] /// Open a `git rebase` like file to select the videos to watch (the default) - File, + File { + /// Include done (watched, dropped) videos + #[arg(long, short)] + done: bool, + }, Watch { #[command(flatten)] shared: SharedSelectionCommandArgs, - #[arg(short, long, default_value = "0")] /// The ordering priority (higher means more at the top) - priority: i64, + #[arg(short, long)] + priority: Option, + + /// The subtitles to download (e.g. 'en,de,sv') + #[arg(short = 'l', long, default_value = constants::DEFAULT_SUBTITLE_LANGS)] + subtitle_langs: String, + + /// The speed to set mpv to + #[arg(short, long, default_value = "2.7")] + speed: f64, }, /// Mark the video given by the hash to be dropped @@ -168,6 +180,11 @@ pub enum SelectCommand { shared: SharedSelectionCommandArgs, }, } +impl Default for SelectCommand { + fn default() -> Self { + Self::File { done: false } + } +} #[derive(Subcommand, Clone, Debug)] pub enum CheckCommand { @@ -178,11 +195,16 @@ pub enum CheckCommand { #[derive(Subcommand, Clone, Copy, Debug)] pub enum CacheCommand { /// Invalidate all cache entries - Invalidate, + Invalidate { + /// Also delete the cache path + #[arg(short, long)] + hard: bool, + }, /// Check every path for validity (removing all invalid cache entries) Maintain { /// Check every video (otherwise only the videos to be watched are checked) + #[arg(short, long)] all: bool, }, } diff --git a/pkgs/by-name/yt/yt/src/constants.rs b/pkgs/by-name/yt/yt/src/constants.rs index fbe51413..e627e552 100644 --- a/pkgs/by-name/yt/yt/src/constants.rs +++ b/pkgs/by-name/yt/yt/src/constants.rs @@ -5,6 +5,10 @@ use anyhow::Context; pub const HELP_STR: &str = include_str!("./select/selection_file/help.str"); pub const LOCAL_COMMENTS_LENGTH: usize = 1000; +// NOTE: KEEP THIS IN SYNC WITH THE `mpv_playback_speed` in `cli.rs` <2024-08-20> +pub const DEFAULT_MPV_PLAYBACK_SPEED: f64 = 2.7; +pub const DEFAULT_SUBTITLE_LANGS: &str = "en"; + pub const CONCURRENT_DOWNLOADS: u32 = 5; // We download to the temp dir to avoid taxing the disk pub fn download_dir() -> PathBuf { diff --git a/pkgs/by-name/yt/yt/src/download/download_options.rs b/pkgs/by-name/yt/yt/src/download/download_options.rs index 4588e774..eb46e78b 100644 --- a/pkgs/by-name/yt/yt/src/download/download_options.rs +++ b/pkgs/by-name/yt/yt/src/download/download_options.rs @@ -1,6 +1,6 @@ -use serde_json::json; +use serde_json::{json, Value}; -use crate::constants; +use crate::{constants, storage::video_database::YtDlpOptions}; // { // "ratelimit": conf.ratelimit if conf.ratelimit > 0 else None, @@ -12,7 +12,7 @@ use crate::constants; // "logger": _ytdl_logger // } -pub fn download_opts() -> serde_json::Map { +pub fn download_opts(additional_opts: YtDlpOptions) -> serde_json::Map { match json!({ "extract_flat": false, "extractor_args": { @@ -39,7 +39,6 @@ pub fn download_opts() -> serde_json::Map { "writesubtitles": true, "writeautomaticsub": true, - "outtmpl": { "default": constants::download_dir().join("%(channel)s/%(title)s.%(ext)s"), "chapter": "%(title)s - %(section_number)03d %(section_title)s [%(id)s].%(ext)s" @@ -91,7 +90,19 @@ pub fn download_opts() -> serde_json::Map { } ] }) { - serde_json::Value::Object(obj) => obj, + serde_json::Value::Object(mut obj) => { + obj.insert( + "subtitleslangs".to_owned(), + serde_json::Value::Array( + additional_opts + .subtitle_langs + .split(',') + .map(|val| Value::String(val.to_owned())) + .collect::>(), + ), + ); + obj + } _ => unreachable!("This is an object"), } } diff --git a/pkgs/by-name/yt/yt/src/download/mod.rs b/pkgs/by-name/yt/yt/src/download/mod.rs index 1499171f..da0dae27 100644 --- a/pkgs/by-name/yt/yt/src/download/mod.rs +++ b/pkgs/by-name/yt/yt/src/download/mod.rs @@ -4,9 +4,7 @@ use crate::{ app::App, download::download_options::download_opts, storage::video_database::{ - downloader::{get_next_uncached_video, set_video_cache_path}, - extractor_hash::ExtractorHash, - Video, + downloader::{get_next_uncached_video, set_video_cache_path}, extractor_hash::ExtractorHash, getters::get_video_yt_dlp_opts, Video }, }; @@ -107,35 +105,12 @@ impl Downloader { Ok(()) } - // pub async fn cache_video(&self, input: Video) -> Result<()> { - // info!("Will cache video: '{}'", &input.title); - // if input.cache_path.is_some() { - // // Value is already cached - // info!( - // "Not caching, as it is already a cached video: '{}'", - // &input.title - // ); - // return Ok(()); - // } else { - // // Store the video to be cached, if resources are available - // set_video_status( - // &input - // .extractor_hash - // .parse() - // .expect("This hash should always be valid"), - // VideoStatus::Caching, - // None, - // ) - // .await?; - // self.cache_sender.send(input).await?; - // - // Ok(()) - // } - // } - async fn actually_cache_video(app: &App, video: &Video) -> Result<()> { debug!("Download started: {}", &video.title); - let result = yt_dlp::download(&[video.url.clone()], &download_opts()) + + let addional_opts = get_video_yt_dlp_opts(&app, &video.extractor_hash).await?; + + let result = yt_dlp::download(&[video.url.clone()], &download_opts(addional_opts)) .await .with_context(|| format!("Failed to download video: '{}'", video.title))?; diff --git a/pkgs/by-name/yt/yt/src/main.rs b/pkgs/by-name/yt/yt/src/main.rs index 92fd1a8d..26af85b2 100644 --- a/pkgs/by-name/yt/yt/src/main.rs +++ b/pkgs/by-name/yt/yt/src/main.rs @@ -2,9 +2,9 @@ use std::fs; use anyhow::{bail, Context, Result}; use app::App; +use cache::invalidate; use clap::Parser; use cli::{CacheCommand, CheckCommand, SelectCommand}; -use log::info; use select::cmds::handle_select_cmd; use yt_dlp::wrapper::info_json::InfoJson; @@ -41,23 +41,19 @@ async fn main() -> Result<()> { let app = App::new().await?; match args.command.unwrap_or(Command::default()) { - Command::Download { urls } => { - if urls.is_empty() { - info!("Downloading urls from database"); - - download::Downloader::new().consume(&app).await?; - } else { - info!("Downloading urls: '{:#?}'", urls); - - todo!() + Command::Download { force } => { + if force { + invalidate(&app, true).await?; } + + download::Downloader::new().consume(&app).await?; } Command::Select { cmd } => { let cmd = cmd.unwrap_or(SelectCommand::default()); match cmd { - SelectCommand::File => select::select(&app).await?, - _ => handle_select_cmd(&app, cmd).await?, + SelectCommand::File { done } => select::select(&app, done).await?, + _ => handle_select_cmd(&app, cmd, None).await?, } } Command::Subscribe { name, url } => { @@ -107,7 +103,7 @@ async fn main() -> Result<()> { Command::Status {} => status::show(&app).await?, Command::Cache { command } => match command { - CacheCommand::Invalidate => cache::invalidate(&app).await?, + CacheCommand::Invalidate { hard } => cache::invalidate(&app, hard).await?, CacheCommand::Maintain { all } => cache::maintain(&app, all).await?, }, diff --git a/pkgs/by-name/yt/yt/src/select/cmds.rs b/pkgs/by-name/yt/yt/src/select/cmds.rs index a2a440f8..a9c18604 100644 --- a/pkgs/by-name/yt/yt/src/select/cmds.rs +++ b/pkgs/by-name/yt/yt/src/select/cmds.rs @@ -1,19 +1,27 @@ use crate::{ app::App, cli::SelectCommand, - storage::video_database::{getters::get_video_by_hash, setters::set_video_status, VideoStatus}, + storage::video_database::{ + getters::get_video_by_hash, + setters::{set_video_options, set_video_status}, + VideoOptions, VideoStatus, + }, }; use anyhow::{Context, Result}; -pub async fn handle_select_cmd(app: &App, cmd: SelectCommand) -> Result<()> { +pub async fn handle_select_cmd( + app: &App, + cmd: SelectCommand, + line_number: Option, +) -> Result<()> { match cmd { SelectCommand::Pick { shared } => { set_video_status( app, &shared.hash.realize(app).await?, VideoStatus::Pick, - None, + line_number, ) .await? } @@ -22,20 +30,34 @@ pub async fn handle_select_cmd(app: &App, cmd: SelectCommand) -> Result<()> { app, &shared.hash.realize(app).await?, VideoStatus::Drop, - None, + line_number, ) .await? } - SelectCommand::Watch { shared, priority } => { + SelectCommand::Watch { + shared, + priority, + subtitle_langs, + speed, + } => { let hash = shared.hash.realize(&app).await?; let video = get_video_by_hash(app, &hash).await?; + let video_options = VideoOptions::new(subtitle_langs, speed); + let priority = if let Some(pri) = priority { + Some(pri) + } else if let Some(pri) = line_number { + Some(pri) + } else { + None + }; if let Some(_) = video.cache_path { - // Do nothing, as the video *should* already have a `Cached` status and a - // cache_path. + set_video_status(app, &hash, VideoStatus::Cached, priority).await?; } else { - set_video_status(app, &hash, VideoStatus::Watch, Some(priority)).await?; + set_video_status(app, &hash, VideoStatus::Watch, priority).await?; } + + set_video_options(app, hash, &video_options).await?; } SelectCommand::Url { shared } => { @@ -44,7 +66,7 @@ pub async fn handle_select_cmd(app: &App, cmd: SelectCommand) -> Result<()> { firefox.arg(shared.url.as_str()); let _handle = firefox.spawn().context("Failed to run firefox")?; } - SelectCommand::File => unreachable!("This should have been filtered out"), + SelectCommand::File { .. } => unreachable!("This should have been filtered out"), } Ok(()) } diff --git a/pkgs/by-name/yt/yt/src/select/mod.rs b/pkgs/by-name/yt/yt/src/select/mod.rs index 34fcf56a..9c250909 100644 --- a/pkgs/by-name/yt/yt/src/select/mod.rs +++ b/pkgs/by-name/yt/yt/src/select/mod.rs @@ -15,29 +15,40 @@ use anyhow::{bail, Context, Result}; use clap::Parser; use cmds::handle_select_cmd; use futures::future::join_all; -use log::debug; use selection_file::process_line; use tempfile::Builder; pub mod cmds; pub mod selection_file; -pub async fn select(app: &App) -> Result<()> { - let matching_videos = get_videos( - app, - // TODO: Flag for deciding about dropped inclusion <2024-06-14> - &[ - VideoStatus::Pick, - // - VideoStatus::Watch, - VideoStatus::Cached, - VideoStatus::Watched, - // - VideoStatus::Drop, - VideoStatus::Dropped, - ], - ) - .await?; +pub async fn select(app: &App, done: bool) -> Result<()> { + let matching_videos = if done { + get_videos( + app, + &[ + VideoStatus::Pick, + // + VideoStatus::Watch, + VideoStatus::Cached, + VideoStatus::Watched, + // + VideoStatus::Drop, + VideoStatus::Dropped, + ], + ) + .await? + } else { + get_videos( + app, + &[ + VideoStatus::Pick, + // + VideoStatus::Watch, + VideoStatus::Cached, + ], + ) + .await? + }; // Warmup the cache for the display rendering of the videos. // Otherwise the futures would all try to warm it up at the same time. @@ -93,16 +104,20 @@ pub async fn select(app: &App) -> Result<()> { let reader = BufReader::new(&read_file); + let mut line_number = 0; for line in reader.lines() { let line = line.context("Failed to read a line")?; + if let Some(line) = process_line(&line)? { - debug!( - "Parsed command: `{}`", - line.iter() - .map(|val| format!("\"{}\"", val)) - .collect::>() - .join(" ") - ); + line_number -= 1; + + // debug!( + // "Parsed command: `{}`", + // line.iter() + // .map(|val| format!("\"{}\"", val)) + // .collect::>() + // .join(" ") + // ); let arg_line = ["yt", "select"] .into_iter() @@ -118,7 +133,12 @@ pub async fn select(app: &App) -> Result<()> { unreachable!("This is checked in the `filter_line` function") }; - handle_select_cmd(&app, cmd.expect("This value should always be some here")).await? + handle_select_cmd( + &app, + cmd.expect("This value should always be some here"), + Some(line_number), + ) + .await? } } diff --git a/pkgs/by-name/yt/yt/src/select/selection_file/display.rs b/pkgs/by-name/yt/yt/src/select/selection_file/display.rs index 3649b2b8..5ab90316 100644 --- a/pkgs/by-name/yt/yt/src/select/selection_file/display.rs +++ b/pkgs/by-name/yt/yt/src/select/selection_file/display.rs @@ -4,7 +4,11 @@ use anyhow::Result; use chrono::DateTime; use log::debug; -use crate::{app::App, select::selection_file::duration::Duration, storage::video_database::Video}; +use crate::{ + app::App, + select::selection_file::duration::Duration, + storage::video_database::{getters::get_video_opts, Video}, +}; macro_rules! c { ($color:expr, $format:expr) => { @@ -16,6 +20,8 @@ impl Video { pub async fn to_select_file_display(&self, app: &App) -> Result { let mut f = String::new(); + let opts = get_video_opts(app, &self.extractor_hash).await?; + let publish_date = if let Some(date) = self.publish_date { DateTime::from_timestamp(date, 0) .expect("This should not fail") @@ -34,8 +40,9 @@ impl Video { debug!("Formatting video for selection file: {}", self.title); write!( f, - r#"{} {} "{}" "{}" "{}" "{}" "{}"{}"#, + r#"{} {} {} "{}" "{}" "{}" "{}" "{}"{}"#, self.status.as_command(), + opts.to_cli_flags(), self.extractor_hash.into_short_hash(app).await?, self.title.replace(['"', '„', '”'], "'"), publish_date, diff --git a/pkgs/by-name/yt/yt/src/storage/video_database/downloader.rs b/pkgs/by-name/yt/yt/src/storage/video_database/downloader.rs index ca3a2ea3..839959fa 100644 --- a/pkgs/by-name/yt/yt/src/storage/video_database/downloader.rs +++ b/pkgs/by-name/yt/yt/src/storage/video_database/downloader.rs @@ -161,7 +161,7 @@ pub async fn set_video_cache_path( Ok(()) } else { debug!( - "Setting cache path from '{}' to 'NULL'", + "Setting cache path from '{}' to NULL", video.into_short_hash(app).await?, ); diff --git a/pkgs/by-name/yt/yt/src/storage/video_database/getters.rs b/pkgs/by-name/yt/yt/src/storage/video_database/getters.rs index cec6c426..57c023e6 100644 --- a/pkgs/by-name/yt/yt/src/storage/video_database/getters.rs +++ b/pkgs/by-name/yt/yt/src/storage/video_database/getters.rs @@ -18,7 +18,7 @@ use crate::{ }, }; -use super::VideoStatus; +use super::{MpvOptions, VideoOptions, VideoStatus, YtDlpOptions}; macro_rules! video_from_record { ($record:expr) => { @@ -252,3 +252,64 @@ pub async fn get_video_hashes(app: &App, subs: &Subscription) -> Result Result { + let ehash = hash.hash().to_string(); + + let yt_dlp_options = query!( + r#" + SELECT subtitle_langs + FROM video_options + WHERE extractor_hash = ?; + "#, + ehash + ) + .fetch_one(&app.database) + .await?; + + Ok(YtDlpOptions { + subtitle_langs: yt_dlp_options.subtitle_langs, + }) +} +pub async fn get_video_mpv_opts(app: &App, hash: &ExtractorHash) -> Result { + let ehash = hash.hash().to_string(); + + let mpv_options = query!( + r#" + SELECT playback_speed + FROM video_options + WHERE extractor_hash = ?; + "#, + ehash + ) + .fetch_one(&app.database) + .await?; + + Ok(MpvOptions { + playback_speed: mpv_options.playback_speed, + }) +} + +pub async fn get_video_opts(app: &App, hash: &ExtractorHash) -> Result { + let ehash = hash.hash().to_string(); + + let opts = query!( + r#" + SELECT playback_speed, subtitle_langs + FROM video_options + WHERE extractor_hash = ?; + "#, + ehash + ) + .fetch_one(&app.database) + .await?; + + let mpv = MpvOptions { + playback_speed: opts.playback_speed, + }; + let yt_dlp = YtDlpOptions { + subtitle_langs: opts.subtitle_langs, + }; + + Ok(VideoOptions { mpv, yt_dlp }) +} diff --git a/pkgs/by-name/yt/yt/src/storage/video_database/mod.rs b/pkgs/by-name/yt/yt/src/storage/video_database/mod.rs index a565fc8d..203cf651 100644 --- a/pkgs/by-name/yt/yt/src/storage/video_database/mod.rs +++ b/pkgs/by-name/yt/yt/src/storage/video_database/mod.rs @@ -1,8 +1,11 @@ -use std::path::PathBuf; +use std::{fmt::Write, path::PathBuf}; use url::Url; -use crate::storage::video_database::extractor_hash::ExtractorHash; +use crate::{ + constants::{DEFAULT_MPV_PLAYBACK_SPEED, DEFAULT_SUBTITLE_LANGS}, + storage::video_database::extractor_hash::ExtractorHash, +}; pub mod downloader; pub mod extractor_hash; @@ -28,6 +31,48 @@ pub struct Video { pub url: Url, } +#[derive(Debug)] +pub struct VideoOptions { + pub yt_dlp: YtDlpOptions, + pub mpv: MpvOptions, +} +impl VideoOptions { + pub(crate) fn new(subtitle_langs: String, playback_speed: f64) -> Self { + let yt_dlp = YtDlpOptions { subtitle_langs }; + let mpv = MpvOptions { playback_speed }; + Self { yt_dlp, mpv } + } + + /// This will write out the options that are different from the defaults. + /// Beware, that this does not set the priority. + pub fn to_cli_flags(self) -> String { + let mut f = String::new(); + + if self.mpv.playback_speed != DEFAULT_MPV_PLAYBACK_SPEED { + write!(f, " --speed '{}'", self.mpv.playback_speed).expect("Works"); + } + if self.yt_dlp.subtitle_langs != DEFAULT_SUBTITLE_LANGS { + write!(f, " --subtitle-langs '{}'", self.yt_dlp.subtitle_langs).expect("Works"); + } + + f.trim().to_owned() + } +} + +#[derive(Debug)] +/// Additionally settings passed to mpv on watch +pub struct MpvOptions { + /// The playback speed. (1 is 100%, 2.7 is 270%, and so on) + pub playback_speed: f64, +} + +#[derive(Debug)] +/// Additionally configuration options, passed to yt-dlp on download +pub struct YtDlpOptions { + /// In the form of `lang1,lang2,lang3` (e.g. `en,de,sv`) + pub subtitle_langs: String, +} + /// # Video Lifetime (words in are commands): /// /// / \ diff --git a/pkgs/by-name/yt/yt/src/storage/video_database/schema.sql b/pkgs/by-name/yt/yt/src/storage/video_database/schema.sql index 2634eef4..d25b7015 100644 --- a/pkgs/by-name/yt/yt/src/storage/video_database/schema.sql +++ b/pkgs/by-name/yt/yt/src/storage/video_database/schema.sql @@ -1,4 +1,5 @@ -- The base schema + -- Keep this table in sync with the `Video` structure CREATE TABLE IF NOT EXISTS videos ( cache_path TEXT UNIQUE CHECK (CASE WHEN cache_path IS NOT NULL THEN status == 2 ELSE 1 END), @@ -15,3 +16,11 @@ CREATE TABLE IF NOT EXISTS videos ( title TEXT NOT NULL, url TEXT UNIQUE NOT NULL ); + +-- Store additional metadata for the videos marked to be watched +CREATE TABLE IF NOT EXISTS video_options ( + extractor_hash TEXT UNIQUE NOT NULL PRIMARY KEY, + subtitle_langs TEXT NOT NULL, + playback_speed REAL NOT NULL, + FOREIGN KEY(extractor_hash) REFERENCES videos (extractor_hash) +) diff --git a/pkgs/by-name/yt/yt/src/storage/video_database/setters.rs b/pkgs/by-name/yt/yt/src/storage/video_database/setters.rs index 251f1e6f..242cf67a 100644 --- a/pkgs/by-name/yt/yt/src/storage/video_database/setters.rs +++ b/pkgs/by-name/yt/yt/src/storage/video_database/setters.rs @@ -3,10 +3,11 @@ use anyhow::Result; use chrono::Utc; use sqlx::query; +use tokio::fs; -use crate::{app::App, storage::video_database::extractor_hash::ExtractorHash}; +use crate::{app::App, constants, storage::video_database::extractor_hash::ExtractorHash}; -use super::{Video, VideoStatus}; +use super::{Video, VideoOptions, VideoStatus}; /// Set a new status for a video. /// This will only update the status time stamp/priority when the status or the priority has changed . @@ -76,10 +77,8 @@ pub async fn set_video_status( /// Mark a video as watched. /// This will both set the status to `Watched` and the cache_path to Null. -pub async fn set_video_watched(app: &App, video_hash: &ExtractorHash) -> Result<()> { - // FIXME: Also delete the cache file <2024-08-19> - - let video_hash = video_hash.hash().to_string(); +pub async fn set_video_watched(app: &App, video: &Video) -> Result<()> { + let video_hash = video.extractor_hash.hash().to_string(); let new_status = VideoStatus::Watched.as_db_integer(); let old = query!( @@ -99,6 +98,12 @@ pub async fn set_video_watched(app: &App, video_hash: &ExtractorHash) -> Result< let now = Utc::now().timestamp(); + if let Some(path) = &video.cache_path { + if let Ok(true) = path.try_exists() { + fs::remove_file(path).await? + } + } + query!( r#" UPDATE videos @@ -138,6 +143,31 @@ pub async fn set_state_change( Ok(()) } +pub async fn set_video_options( + app: &App, + hash: ExtractorHash, + video_options: &VideoOptions, +) -> Result<()> { + let video_extractor_hash = hash.hash().to_string(); + let playback_speed = video_options.mpv.playback_speed; + let subtitle_langs = &video_options.yt_dlp.subtitle_langs; + + query!( + r#" + UPDATE video_options + SET playback_speed = ?, subtitle_langs = ? + WHERE extractor_hash = ?; + "#, + playback_speed, + subtitle_langs, + video_extractor_hash, + ) + .execute(&app.database) + .await?; + + Ok(()) +} + pub async fn add_video(app: &App, video: Video) -> Result<()> { let parent_subscription_name = if let Some(subs) = video.parent_subscription_name { subs @@ -156,8 +186,12 @@ pub async fn add_video(app: &App, video: Video) -> Result<()> { let url = video.url.to_string(); let extractor_hash = video.extractor_hash.hash().to_string(); + let default_subtitle_langs = constants::DEFAULT_SUBTITLE_LANGS; + let default_mpv_playback_speed = constants::DEFAULT_MPV_PLAYBACK_SPEED; + query!( r#" + BEGIN; INSERT INTO videos ( parent_subscription_name, status, @@ -171,6 +205,13 @@ pub async fn add_video(app: &App, video: Video) -> Result<()> { thumbnail_url, extractor_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + + INSERT INTO video_options ( + extractor_hash, + subtitle_langs, + playback_speed) + VALUES (?, ?, ?); + COMMIT; "#, parent_subscription_name, status, @@ -182,7 +223,10 @@ pub async fn add_video(app: &App, video: Video) -> Result<()> { video.duration, video.publish_date, thumbnail_url, - extractor_hash + extractor_hash, + extractor_hash, + default_subtitle_langs, + default_mpv_playback_speed ) .execute(&app.database) .await?; diff --git a/pkgs/by-name/yt/yt/src/watch/events.rs b/pkgs/by-name/yt/yt/src/watch/events.rs index cab6807f..d693fdce 100644 --- a/pkgs/by-name/yt/yt/src/watch/events.rs +++ b/pkgs/by-name/yt/yt/src/watch/events.rs @@ -1,4 +1,4 @@ -use std::{env::current_exe, usize}; +use std::{env::current_exe, mem, usize}; use anyhow::{bail, Result}; use libmpv2::{events::Event, EndFileReason, Mpv}; @@ -11,6 +11,7 @@ use crate::{ constants::LOCAL_COMMENTS_LENGTH, storage::video_database::{ extractor_hash::ExtractorHash, + getters::{get_video_by_hash, get_video_mpv_opts}, setters::{set_state_change, set_video_watched}, }, }; @@ -30,10 +31,15 @@ impl MpvEventHandler { } } + 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]; - set_video_watched(&app, video_hash).await?; + let video_hash = self.current_playlist[(index) as usize].clone(); + self.mark_video_watched(app, &video_hash).await?; } Ok(()) } @@ -53,6 +59,15 @@ impl MpvEventHandler { 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, @@ -72,8 +87,12 @@ impl MpvEventHandler { EndFileReason::Quit => { info!("Mpv quit. Exiting playback"); - self.mark_cvideo_watched(app).await?; self.mark_cvideo_inactive(app).await?; + // 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?; + } return Ok(true); } EndFileReason::Error => { @@ -87,6 +106,12 @@ impl MpvEventHandler { 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) => { diff --git a/pkgs/by-name/yt/yt/src/watch/mod.rs b/pkgs/by-name/yt/yt/src/watch/mod.rs index 3a4f8983..4b558968 100644 --- a/pkgs/by-name/yt/yt/src/watch/mod.rs +++ b/pkgs/by-name/yt/yt/src/watch/mod.rs @@ -15,6 +15,8 @@ pub mod events; pub async fn watch(app: &App) -> Result<()> { maintain(app, false).await?; + // set some default values, to make things easier (these can be overridden by the config file, + // which we load later) let mpv = Mpv::with_initializer(|mpv| { // Enable default key bindings, so the user can actually interact with // the player (and e.g. close the window). diff --git a/pkgs/by-name/yt/yt/yt_dlp/src/wrapper/info_json.rs b/pkgs/by-name/yt/yt/yt_dlp/src/wrapper/info_json.rs index e9bf0402..9b50aa93 100644 --- a/pkgs/by-name/yt/yt/yt_dlp/src/wrapper/info_json.rs +++ b/pkgs/by-name/yt/yt/yt_dlp/src/wrapper/info_json.rs @@ -425,6 +425,7 @@ pub struct ThumbNail { #[serde(deny_unknown_fields)] pub struct Format { pub __needs_testing: Option, + pub __working: Option, pub abr: Option, pub acodec: Option, pub aspect_ratio: Option, @@ -490,6 +491,7 @@ pub struct HttpHeader { pub struct Fragment { pub url: String, pub duration: f64, + pub path: Option, } impl InfoJson { -- cgit 1.4.1