about summary refs log tree commit diff stats
path: root/pkgs
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-08-20 15:37:41 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-08-20 15:39:38 +0200
commitdbfabfc078dea5d1ec30cd3e7ae94f76a8065116 (patch)
tree22be35de3b62cabe1628fe751c004de5ff680e43 /pkgs
parentfix(pkgs/yt): Improve usability (diff)
downloadnixos-config-dbfabfc078dea5d1ec30cd3e7ae94f76a8065116.tar.gz
nixos-config-dbfabfc078dea5d1ec30cd3e7ae94f76a8065116.zip
feat(pkgs/yt): Implement additional features and bug fixes
Diffstat (limited to 'pkgs')
-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 {