aboutsummaryrefslogtreecommitdiffstats
path: root/pkgs/by-name
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/by-name')
-rwxr-xr-xpkgs/by-name/yt/yt/scripts/mkdb.sh1
-rw-r--r--pkgs/by-name/yt/yt/src/cache/mod.rs26
-rw-r--r--pkgs/by-name/yt/yt/src/cli.rs40
-rw-r--r--pkgs/by-name/yt/yt/src/constants.rs4
-rw-r--r--pkgs/by-name/yt/yt/src/download/download_options.rs21
-rw-r--r--pkgs/by-name/yt/yt/src/download/mod.rs35
-rw-r--r--pkgs/by-name/yt/yt/src/main.rs22
-rw-r--r--pkgs/by-name/yt/yt/src/select/cmds.rs40
-rw-r--r--pkgs/by-name/yt/yt/src/select/mod.rs70
-rw-r--r--pkgs/by-name/yt/yt/src/select/selection_file/display.rs11
-rw-r--r--pkgs/by-name/yt/yt/src/storage/video_database/downloader.rs2
-rw-r--r--pkgs/by-name/yt/yt/src/storage/video_database/getters.rs63
-rw-r--r--pkgs/by-name/yt/yt/src/storage/video_database/mod.rs49
-rw-r--r--pkgs/by-name/yt/yt/src/storage/video_database/schema.sql9
-rw-r--r--pkgs/by-name/yt/yt/src/storage/video_database/setters.rs58
-rw-r--r--pkgs/by-name/yt/yt/src/watch/events.rs33
-rw-r--r--pkgs/by-name/yt/yt/src/watch/mod.rs2
-rw-r--r--pkgs/by-name/yt/yt/yt_dlp/src/wrapper/info_json.rs2
18 files changed, 370 insertions, 118 deletions
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<Url>,
+ /// 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<i64>,
+
+ /// 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<String, serde_json::Value> {
+pub fn download_opts(additional_opts: YtDlpOptions) -> serde_json::Map<String, serde_json::Value> {
match json!({
"extract_flat": false,
"extractor_args": {
@@ -39,7 +39,6 @@ pub fn download_opts() -> serde_json::Map<String, serde_json::Value> {
"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<String, serde_json::Value> {
}
]
}) {
- 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::<Vec<_>>(),
+ ),
+ );
+ 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<i64>,
+) -> 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::<Vec<String>>()
- .join(" ")
- );
+ line_number -= 1;
+
+ // debug!(
+ // "Parsed command: `{}`",
+ // line.iter()
+ // .map(|val| format!("\"{}\"", val))
+ // .collect::<Vec<String>>()
+ // .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<String> {
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<Vec<Hash
})
.collect())
}
+
+pub async fn get_video_yt_dlp_opts(app: &App, hash: &ExtractorHash) -> Result<YtDlpOptions> {
+ 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<MpvOptions> {
+ 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<VideoOptions> {
+ 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 <brackets> are commands):
/// <Pick>
/// / \
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<bool>,
+ pub __working: Option<Todo>,
pub abr: Option<f64>,
pub acodec: Option<String>,
pub aspect_ratio: Option<f64>,
@@ -490,6 +491,7 @@ pub struct HttpHeader {
pub struct Fragment {
pub url: String,
pub duration: f64,
+ pub path: Option<PathBuf>,
}
impl InfoJson {