about summary refs log tree commit diff stats
path: root/crates
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-07-14 16:03:50 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-07-14 16:03:50 +0200
commite4d6fc04f60cf7b8173df7f261428b25d009ba39 (patch)
tree236abaac0f41f75391e3b7048920168593f31608 /crates
parentrefactor(crates/yt): Make every `pub` item `pub(crate)` (diff)
downloadyt-e4d6fc04f60cf7b8173df7f261428b25d009ba39.zip
feat(crates/yt/storage): Migrate inserts to operations and use methods
This allows us to re-use the operations and in the future to provide
undo-capabilities and a git-reflog like changelog.

This commit also fixes some bugs with the old design.
Diffstat (limited to 'crates')
-rw-r--r--crates/yt/src/storage/db/extractor_hash.rs (renamed from crates/yt/src/storage/video_database/extractor_hash.rs)87
-rw-r--r--crates/yt/src/storage/db/get/extractor_hash.rs58
-rw-r--r--crates/yt/src/storage/db/get/mod.rs4
-rw-r--r--crates/yt/src/storage/db/get/playlist.rs58
-rw-r--r--crates/yt/src/storage/db/get/subscription.rs39
-rw-r--r--crates/yt/src/storage/db/get/video/mod.rs188
-rw-r--r--crates/yt/src/storage/db/insert/mod.rs73
-rw-r--r--crates/yt/src/storage/db/insert/playlist.rs207
-rw-r--r--crates/yt/src/storage/db/insert/subscription.rs84
-rw-r--r--crates/yt/src/storage/db/insert/video/mod.rs599
-rw-r--r--crates/yt/src/storage/db/mod.rs7
-rw-r--r--crates/yt/src/storage/db/playlist/mod.rs49
-rw-r--r--crates/yt/src/storage/db/subscription.rs41
-rw-r--r--crates/yt/src/storage/db/video.rs (renamed from crates/yt/src/storage/video_database/mod.rs)206
-rw-r--r--crates/yt/src/storage/notify.rs (renamed from crates/yt/src/storage/video_database/notify.rs)4
-rw-r--r--crates/yt/src/storage/subscriptions.rs141
-rw-r--r--crates/yt/src/storage/video_database/downloader.rs130
-rw-r--r--crates/yt/src/storage/video_database/get/mod.rs307
-rw-r--r--crates/yt/src/storage/video_database/get/playlist/iterator.rs101
-rw-r--r--crates/yt/src/storage/video_database/get/playlist/mod.rs167
-rw-r--r--crates/yt/src/storage/video_database/set/mod.rs327
-rw-r--r--crates/yt/src/storage/video_database/set/playlist.rs101
22 files changed, 1573 insertions, 1405 deletions
diff --git a/crates/yt/src/storage/video_database/extractor_hash.rs b/crates/yt/src/storage/db/extractor_hash.rs
index df545d7..abe1f0f 100644
--- a/crates/yt/src/storage/video_database/extractor_hash.rs
+++ b/crates/yt/src/storage/db/extractor_hash.rs
@@ -15,13 +15,14 @@ use anyhow::{Context, Result, bail};
 use blake3::Hash;
 use log::debug;
 use tokio::sync::OnceCell;
+use yt_dlp::{info_json::InfoJson, json_cast, json_get};
 
-use crate::{app::App, storage::video_database::get::get_all_hashes, unreachable::Unreachable};
+use crate::app::App;
 
 static EXTRACTOR_HASH_LENGTH: OnceCell<usize> = OnceCell::const_new();
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)]
-pub struct ExtractorHash {
+pub(crate) struct ExtractorHash {
     hash: Hash,
 }
 
@@ -32,7 +33,7 @@ impl Display for ExtractorHash {
 }
 
 #[derive(Debug, Clone)]
-pub struct ShortHash(String);
+pub(crate) struct ShortHash(String);
 
 impl Display for ShortHash {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@@ -42,7 +43,7 @@ impl Display for ShortHash {
 
 #[derive(Debug, Clone)]
 #[allow(clippy::module_name_repetitions)]
-pub struct LazyExtractorHash {
+pub(crate) struct LazyExtractorHash {
     value: ShortHash,
 }
 
@@ -63,28 +64,78 @@ impl FromStr for LazyExtractorHash {
 
 impl LazyExtractorHash {
     /// Turn the [`LazyExtractorHash`] into the [`ExtractorHash`]
-    pub async fn realize(self, app: &App) -> Result<ExtractorHash> {
+    pub(crate) async fn realize(self, app: &App) -> Result<ExtractorHash> {
         ExtractorHash::from_short_hash(app, &self.value).await
     }
 }
 
 impl ExtractorHash {
     #[must_use]
-    pub fn from_hash(hash: Hash) -> Self {
+    pub(crate) fn from_hash(hash: Hash) -> Self {
         Self { hash }
     }
-    pub async fn from_short_hash(app: &App, s: &ShortHash) -> Result<Self> {
+
+    pub(crate) async fn from_short_hash(app: &App, s: &ShortHash) -> Result<Self> {
         Ok(Self {
-            hash: Self::short_hash_to_full_hash(app, s).await?,
+            hash: Self::short_hash_to_full_hash(app, s).await?.hash,
         })
     }
 
+    pub(crate) fn from_info_json(entry: &InfoJson) -> Self {
+        // HACK(@bpeetz): The code that follows is a gross hack.
+        // One would expect the `id` to be unique _and_ constant for each and every possible info JSON.
+        // But .. it's just not. The `ARDMediathek` extractor, will sometimes return different `id`s for the same
+        // video, effectively causing us to insert the same video again into the db (which fails,
+        // because the URL is still unique).
+        //
+        // As such we _should_ probably find a constant value for all extractors, but that just does
+        // not exist currently, without processing each entry (which is expensive and which I would
+        // like to avoid).
+        //
+        // Therefor, we simply special case the `ARDBetaMediathek` extractor. <2025-07-04>
+
+        // NOTE(@bpeetz): `yt-dlp` apparently uses these two different names for the same thing <2025-07-04>
+        let ie_key = {
+            if let Some(val) = entry.get("ie_key") {
+                json_cast!(val, as_str)
+            } else if let Some(val) = entry.get("extractor_key") {
+                json_cast!(val, as_str)
+            } else {
+                unreachable!(
+                    "Either `ie_key` or `extractor_key` \
+                should be set on every entry info json"
+                )
+            }
+        };
+
+        if ie_key == "ARDBetaMediathek" {
+            // NOTE(@bpeetz): The mediathek is changing their Id scheme, from an `short` old Id to the
+            // new id. As the new id is too long for some people, yt-dlp will be default return the old
+            // one (when it is still available!). The new one is called `display_id`.
+            // Therefore, we simply check if the new one is explicitly returned, and otherwise use the
+            // normal `id` value, as these are cases where the old one is no longer available. <2025-07-04>
+            let id = if let Some(val) = entry.get("display_id") {
+                json_cast!(val, as_str).as_bytes()
+            } else {
+                json_get!(entry, "id", as_str).as_bytes()
+            };
+
+            Self {
+                hash: blake3::hash(id),
+            }
+        } else {
+            Self {
+                hash: blake3::hash(json_get!(entry, "id", as_str).as_bytes()),
+            }
+        }
+    }
+
     #[must_use]
-    pub fn hash(&self) -> &Hash {
+    pub(crate) fn hash(&self) -> &Hash {
         &self.hash
     }
 
-    pub async fn into_short_hash(&self, app: &App) -> Result<ShortHash> {
+    pub(crate) async fn into_short_hash(&self, app: &App) -> Result<ShortHash> {
         let needed_chars = if let Some(needed_chars) = EXTRACTOR_HASH_LENGTH.get() {
             *needed_chars
         } else {
@@ -92,9 +143,9 @@ impl ExtractorHash {
                 .get_needed_char_len(app)
                 .await
                 .context("Failed to calculate needed char length")?;
-            EXTRACTOR_HASH_LENGTH.set(needed_chars).unreachable(
-                "This should work at this stage, as we checked above that it is empty.",
-            );
+            EXTRACTOR_HASH_LENGTH
+                .set(needed_chars)
+                .expect("This should work at this stage, as we checked above that it is empty.");
 
             needed_chars
         };
@@ -108,15 +159,15 @@ impl ExtractorHash {
         ))
     }
 
-    async fn short_hash_to_full_hash(app: &App, s: &ShortHash) -> Result<Hash> {
-        let all_hashes = get_all_hashes(app)
+    async fn short_hash_to_full_hash(app: &App, s: &ShortHash) -> Result<Self> {
+        let all_hashes = Self::get_all(app)
             .await
             .context("Failed to fetch all extractor -hashesh from database")?;
 
         let needed_chars = s.0.len();
 
         for hash in all_hashes {
-            if hash.to_hex()[..needed_chars] == s.0 {
+            if hash.hash().to_hex()[..needed_chars] == s.0 {
                 return Ok(hash);
             }
         }
@@ -126,13 +177,13 @@ impl ExtractorHash {
 
     async fn get_needed_char_len(&self, app: &App) -> Result<usize> {
         debug!("Calculating the needed hash char length");
-        let all_hashes = get_all_hashes(app)
+        let all_hashes = Self::get_all(app)
             .await
             .context("Failed to fetch all extractor -hashesh from database")?;
 
         let all_char_vec_hashes = all_hashes
             .into_iter()
-            .map(|hash| hash.to_hex().chars().collect::<Vec<char>>())
+            .map(|hash| hash.hash().to_hex().chars().collect::<Vec<char>>())
             .collect::<Vec<Vec<_>>>();
 
         // This value should be updated later, if not rust will panic in the assertion.
diff --git a/crates/yt/src/storage/db/get/extractor_hash.rs b/crates/yt/src/storage/db/get/extractor_hash.rs
new file mode 100644
index 0000000..d10b326
--- /dev/null
+++ b/crates/yt/src/storage/db/get/extractor_hash.rs
@@ -0,0 +1,58 @@
+use anyhow::Result;
+use blake3::Hash;
+use sqlx::{SqliteConnection, query};
+
+use crate::{
+    app::App,
+    storage::db::{
+        extractor_hash::ExtractorHash,
+        video::{Video, video_from_record},
+    },
+};
+
+impl ExtractorHash {
+    pub(crate) async fn get(&self, txn: &mut SqliteConnection) -> Result<Video> {
+        let extractor_hash = self.hash().to_string();
+
+        let base = query!(
+            r#"
+            SELECT *
+            FROM videos
+            WHERE extractor_hash = ?
+            "#,
+            extractor_hash
+        )
+        .fetch_one(txn)
+        .await?;
+
+        Ok(video_from_record!(base))
+    }
+
+    pub(crate) async fn get_with_app(&self, app: &App) -> Result<Video> {
+        let mut txn = app.database.begin().await?;
+        let out = self.get(&mut txn).await?;
+        txn.commit().await?;
+
+        Ok(out)
+    }
+
+    pub(crate) async fn get_all(app: &App) -> Result<Vec<Self>> {
+        let hashes_hex = query!(
+            r#"
+        SELECT extractor_hash
+        FROM videos;
+        "#
+        )
+        .fetch_all(&app.database)
+        .await?;
+
+        Ok(hashes_hex
+            .iter()
+            .map(|hash| {
+                Self::from_hash(Hash::from_hex(&hash.extractor_hash).expect(
+                    "These values started as blake3 hashes, they should stay blake3 hashes",
+                ))
+            })
+            .collect())
+    }
+}
diff --git a/crates/yt/src/storage/db/get/mod.rs b/crates/yt/src/storage/db/get/mod.rs
new file mode 100644
index 0000000..8ca3075
--- /dev/null
+++ b/crates/yt/src/storage/db/get/mod.rs
@@ -0,0 +1,4 @@
+pub(crate) mod subscription;
+pub(crate) mod video;
+pub(crate) mod extractor_hash;
+pub(crate) mod playlist;
diff --git a/crates/yt/src/storage/db/get/playlist.rs b/crates/yt/src/storage/db/get/playlist.rs
new file mode 100644
index 0000000..95a61bf
--- /dev/null
+++ b/crates/yt/src/storage/db/get/playlist.rs
@@ -0,0 +1,58 @@
+use crate::{
+    app::App,
+    storage::db::{
+        playlist::{Playlist, PlaylistIndex},
+        video::{Video, VideoStatusMarker},
+    },
+};
+
+use anyhow::Result;
+
+impl Playlist {
+    /// Get an video based in its index.
+    #[must_use]
+    pub(crate) fn get_mut(&mut self, index: PlaylistIndex) -> Option<&mut Video> {
+        self.videos.get_mut(Into::<usize>::into(index))
+    }
+
+    /// Create a playlist, by loading it from the database.
+    pub(crate) async fn create(app: &App) -> Result<Self> {
+        let videos = Video::in_states(app, &[VideoStatusMarker::Cached]).await?;
+
+        Ok(Self { videos })
+    }
+
+    /// Return the current playlist index.
+    ///
+    /// This effectively looks for the currently focused video and returns it's index.
+    ///
+    /// # Panics
+    /// Only if internal assertions fail.
+    pub(crate) fn current_index(&self) -> Option<PlaylistIndex> {
+        if let Some((index, _)) = self.get_focused() {
+            Some(index)
+        } else {
+            None
+        }
+    }
+
+    /// Get the currently focused video, if it exists.
+    #[must_use]
+    pub(crate) fn get_focused_mut(&mut self) -> Option<(PlaylistIndex, &mut Video)> {
+        self.videos
+            .iter_mut()
+            .enumerate()
+            .find(|(_, v)| v.is_focused())
+            .map(|(index, video)| (PlaylistIndex::from(index), video))
+    }
+
+    /// Get the currently focused video, if it exists.
+    #[must_use]
+    pub(crate) fn get_focused(&self) -> Option<(PlaylistIndex, &Video)> {
+        self.videos
+            .iter()
+            .enumerate()
+            .find(|(_, v)| v.is_focused())
+            .map(|(index, video)| (PlaylistIndex::from(index), video))
+    }
+}
diff --git a/crates/yt/src/storage/db/get/subscription.rs b/crates/yt/src/storage/db/get/subscription.rs
new file mode 100644
index 0000000..cdb2b9a
--- /dev/null
+++ b/crates/yt/src/storage/db/get/subscription.rs
@@ -0,0 +1,39 @@
+use std::collections::HashMap;
+
+use crate::{
+    app::App,
+    storage::db::subscription::{Subscription, Subscriptions},
+};
+
+use anyhow::Result;
+use sqlx::query;
+use url::Url;
+
+impl Subscriptions {
+    /// Get a list of subscriptions
+    pub(crate) async fn get(app: &App) -> Result<Self> {
+        let raw_subs = query!(
+            "
+        SELECT *
+        FROM subscriptions;
+        "
+        )
+        .fetch_all(&app.database)
+        .await?;
+
+        let subscriptions: HashMap<String, Subscription> = raw_subs
+            .into_iter()
+            .map(|sub| {
+                (
+                    sub.name.clone(),
+                    Subscription::new(
+                        sub.name,
+                        Url::parse(&sub.url).expect("It was an URL, when we inserted it."),
+                    ),
+                )
+            })
+            .collect();
+
+        Ok(Subscriptions(subscriptions))
+    }
+}
diff --git a/crates/yt/src/storage/db/get/video/mod.rs b/crates/yt/src/storage/db/get/video/mod.rs
new file mode 100644
index 0000000..ec00934
--- /dev/null
+++ b/crates/yt/src/storage/db/get/video/mod.rs
@@ -0,0 +1,188 @@
+use std::{fs::File, path::PathBuf};
+
+use anyhow::{Context, Result, bail};
+use log::debug;
+use sqlx::query;
+use yt_dlp::info_json::InfoJson;
+
+use crate::{
+    app::App,
+    storage::db::video::{Video, VideoStatus, VideoStatusMarker, video_from_record},
+};
+
+impl Video {
+    /// Returns to next video which should be downloaded. This respects the priority assigned by select.
+    /// It does not return videos, which are already downloaded.
+    ///
+    /// # Panics
+    /// Only if assertions fail.
+    pub(crate) async fn next_to_download(app: &App) -> Result<Option<Self>> {
+        let status = VideoStatus::Watch.as_marker().as_db_integer();
+
+        // NOTE: The ORDER BY statement should be the same as the one in [`in_states`]. <2024-08-22>
+        let result = query!(
+            r#"
+        SELECT *
+        FROM videos
+        WHERE status = ? AND cache_path IS NULL
+        ORDER BY priority DESC, publish_date DESC
+        LIMIT 1;
+    "#,
+            status
+        )
+        .fetch_one(&app.database)
+        .await;
+
+        if let Err(sqlx::Error::RowNotFound) = result {
+            Ok(None)
+        } else {
+            let base = result?;
+
+            Ok(Some(video_from_record!(base)))
+        }
+    }
+
+    /// Optionally returns the video that is currently focused.
+    ///
+    /// # Panics
+    /// Only if assertions fail.
+    pub(crate) async fn currently_focused(app: &App) -> Result<Option<Self>> {
+        let status = VideoStatusMarker::Cached.as_db_integer();
+
+        let result = query!(
+            r#"
+            SELECT *
+            FROM videos
+            WHERE status = ? AND is_focused = 1
+            "#,
+            status
+        )
+        .fetch_one(&app.database)
+        .await;
+
+        if let Err(sqlx::Error::RowNotFound) = result {
+            Ok(None)
+        } else {
+            let base = result?;
+
+            Ok(Some(video_from_record!(base)))
+        }
+    }
+
+    /// Calculate the [`info_json`] location on-disk for this video.
+    ///
+    /// Will return [`None`], if the video does not have an downloaded [`info_json`]
+    pub(crate) fn info_json_path(&self) -> Result<Option<PathBuf>> {
+        if let VideoStatus::Cached { mut cache_path, .. } = self.status.clone() {
+            if !cache_path.set_extension("info.json") {
+                bail!(
+                    "Failed to change path extension to 'info.json': {}",
+                    cache_path.display()
+                );
+            }
+
+            Ok(Some(cache_path))
+        } else {
+            Ok(None)
+        }
+    }
+
+    /// Fetch the [`info_json`], downloaded on-disk for this video.
+    ///
+    /// Will return [`None`], if the video does not have an downloaded [`info_json`]
+    pub(crate) fn get_info_json(&self) -> Result<Option<InfoJson>> {
+        if let Some(path) = self.info_json_path()? {
+            let info_json_string = File::open(path)?;
+            let info_json = serde_json::from_reader(&info_json_string)?;
+
+            Ok(Some(info_json))
+        } else {
+            Ok(None)
+        }
+    }
+
+    /// Returns this videos `is_focused` flag if it is set.
+    ///
+    /// Will return `false` for not-downloaded videos.
+    pub(crate) fn is_focused(&self) -> bool {
+        if let VideoStatus::Cached { is_focused, .. } = &self.status {
+            *is_focused
+        } else {
+            false
+        }
+    }
+
+    /// Returns the videos that are in the `allowed_states`.
+    ///
+    /// # Panics
+    /// Only, if assertions fail.
+    pub(crate) async fn in_states(
+        app: &App,
+        allowed_states: &[VideoStatusMarker],
+    ) -> Result<Vec<Video>> {
+        fn test(all_states: &[VideoStatusMarker], check: VideoStatusMarker) -> Option<i64> {
+            if all_states.contains(&check) {
+                Some(check.as_db_integer())
+            } else {
+                None
+            }
+        }
+        fn states_to_string(allowed_states: &[VideoStatusMarker]) -> String {
+            let mut states = allowed_states
+                .iter()
+                .fold(String::from("&["), |mut acc, state| {
+                    acc.push_str(state.as_str());
+                    acc.push_str(", ");
+                    acc
+                });
+            states = states.trim().to_owned();
+            states = states.trim_end_matches(',').to_owned();
+            states.push(']');
+            states
+        }
+
+        debug!(
+            "Fetching videos in the states: '{}'",
+            states_to_string(allowed_states)
+        );
+        let active_pick: Option<i64> = test(allowed_states, VideoStatusMarker::Pick);
+        let active_watch: Option<i64> = test(allowed_states, VideoStatusMarker::Watch);
+        let active_cached: Option<i64> = test(allowed_states, VideoStatusMarker::Cached);
+        let active_watched: Option<i64> = test(allowed_states, VideoStatusMarker::Watched);
+        let active_drop: Option<i64> = test(allowed_states, VideoStatusMarker::Drop);
+        let active_dropped: Option<i64> = test(allowed_states, VideoStatusMarker::Dropped);
+
+        // NOTE: The ORDER BY statement should be the same as the one in [`next_to_download`]. <2024-08-22>
+        let videos = query!(
+            r"
+          SELECT *
+          FROM videos
+          WHERE status IN (?,?,?,?,?,?)
+          ORDER BY priority DESC, publish_date DESC;
+          ",
+            active_pick,
+            active_watch,
+            active_cached,
+            active_watched,
+            active_drop,
+            active_dropped,
+        )
+        .fetch_all(&app.database)
+        .await
+        .with_context(|| {
+            format!(
+                "Failed to query videos with states: '{}'",
+                states_to_string(allowed_states)
+            )
+        })?;
+
+        let real_videos: Vec<Video> = videos
+            .iter()
+            .map(|base| -> Video {
+                video_from_record!(base)
+            })
+            .collect();
+
+        Ok(real_videos)
+    }
+}
diff --git a/crates/yt/src/storage/db/insert/mod.rs b/crates/yt/src/storage/db/insert/mod.rs
new file mode 100644
index 0000000..f1d464f
--- /dev/null
+++ b/crates/yt/src/storage/db/insert/mod.rs
@@ -0,0 +1,73 @@
+use std::mem;
+
+use crate::app::App;
+
+use anyhow::Result;
+use log::trace;
+use sqlx::SqliteConnection;
+
+pub(crate) mod playlist;
+pub(crate) mod subscription;
+pub(crate) mod video;
+
+pub(crate) trait Committable: Sized {
+    async fn commit(self, txn: &mut SqliteConnection) -> Result<()>;
+}
+
+#[derive(Debug)]
+pub(crate) struct Operations<O: Committable + std::fmt::Debug> {
+    name: &'static str,
+    ops: Vec<O>,
+}
+
+impl<O: Committable + std::fmt::Debug> Default for Operations<O> {
+    fn default() -> Self {
+        Self::new("<default impl>")
+    }
+}
+
+impl<O: Committable + std::fmt::Debug> Operations<O> {
+    #[must_use]
+    pub(crate) fn new(name: &'static str) -> Self {
+        Self {
+            name,
+            ops: Vec::new(),
+        }
+    }
+
+    pub(crate) async fn commit(mut self, app: &App) -> Result<()> {
+        let ops = mem::take(&mut self.ops);
+
+        if ops.is_empty() {
+            return Ok(());
+        }
+
+        trace!("Begin commit of {}", self.name);
+        let mut txn = app.database.begin().await?;
+
+        for op in ops {
+            trace!("Commiting operation: {op:?}");
+            op.commit(&mut txn).await?;
+        }
+
+        txn.commit().await?;
+        trace!("End commit of {}", self.name);
+
+        Ok(())
+    }
+
+    pub(crate) fn push(&mut self, op: O) {
+        self.ops.push(op);
+    }
+}
+
+impl<O: Committable + std::fmt::Debug> Drop for Operations<O> {
+    fn drop(&mut self) {
+        assert!(
+            self.ops.is_empty(),
+            "Trying to drop uncommitted operations (name: {}) ({:#?}). This is a bug.",
+            self.name,
+            self.ops
+        );
+    }
+}
diff --git a/crates/yt/src/storage/db/insert/playlist.rs b/crates/yt/src/storage/db/insert/playlist.rs
new file mode 100644
index 0000000..2613fb3
--- /dev/null
+++ b/crates/yt/src/storage/db/insert/playlist.rs
@@ -0,0 +1,207 @@
+use std::{cmp::Ordering, time::Duration};
+
+use anyhow::{Context, Result};
+use libmpv2::Mpv;
+use log::{debug, trace};
+
+use crate::{
+    app::App,
+    storage::db::{
+        insert::{Operations, video::Operation},
+        playlist::{Playlist, PlaylistIndex},
+        video::VideoStatus,
+    },
+};
+
+#[derive(Debug, Clone, Copy)]
+pub(crate) enum VideoTransition {
+    Watched,
+    Picked,
+}
+
+impl Playlist {
+    pub(crate) fn mark_current_done(
+        &mut self,
+        app: &App,
+        mpv: &Mpv,
+        new_state: VideoTransition,
+        ops: &mut Operations<Operation>,
+    ) -> Result<()> {
+        let (current_index, current_video) = self
+            .get_focused_mut()
+            .expect("This should be some at this point");
+
+        debug!(
+            "Playlist handler will mark video '{}' {:?}.",
+            current_video.title, new_state
+        );
+
+        match new_state {
+            VideoTransition::Watched => current_video.set_watched(ops),
+            VideoTransition::Picked => current_video.set_status(VideoStatus::Pick, ops),
+        }
+
+        self.save_watch_progress(mpv, current_index, ops);
+
+        self.videos.remove(Into::<usize>::into(current_index));
+
+        {
+            // Decide which video to mark focused now.
+            let index = usize::from(current_index);
+            let playlist_length = self.len();
+
+            let index = match index.cmp(&playlist_length) {
+                Ordering::Greater => {
+                    unreachable!(
+                        "The index '{index}' cannot exceed the \
+                        playlist length '{playlist_length}' as indices are 0 based."
+                    );
+                }
+                Ordering::Less => {
+                    // The index is still valid.
+                    // Therefore, we keep the user at this position.
+                    index
+                }
+                Ordering::Equal => {
+                    // The index is pointing to the end of the playlist. We could either go the second
+                    // to last entry (i.e., one entry back) or wrap around to the start.
+                    // We wrap around.
+                    0
+                }
+            };
+
+            let next = self
+                .get_mut(PlaylistIndex::from(index))
+                .expect("We checked that the index is still good");
+            next.set_focused(true, ops);
+
+            // Tell mpv about our decision.
+            self.resync_with_mpv(app, mpv)?;
+        }
+
+        Ok(())
+    }
+
+    /// Sync the mpv playlist with this playlist.
+    pub(crate) fn resync_with_mpv(&self, app: &App, mpv: &Mpv) -> Result<()> {
+        fn get_playlist_count(mpv: &Mpv) -> Result<usize> {
+            mpv.get_property::<i64>("playlist/count")
+                .context("Failed to get mpv playlist len")
+                .map(|count| {
+                    usize::try_from(count).expect("The playlist_count should always be positive")
+                })
+        }
+
+        if get_playlist_count(mpv)? != 0 {
+            // We could also use `loadlist`, but that would require use to start a unix socket or even
+            // write all the video paths to a file beforehand
+            mpv.command("playlist-clear", &[])?;
+            mpv.command("playlist-remove", &["current"])?;
+        }
+
+        assert_eq!(
+            get_playlist_count(mpv)?,
+            0,
+            "The playlist should be empty at this point."
+        );
+
+        debug!("MpvReload: Adding {} videos to playlist.", self.len());
+
+        self.videos
+            .iter()
+            .enumerate()
+            .try_for_each(|(index, video)| {
+                let VideoStatus::Cached {
+                    cache_path,
+                    is_focused,
+                } = &video.status
+                else {
+                    unreachable!("All of the videos in a playlist are cached");
+                };
+
+                let options = format!(
+                    "speed={},start={}",
+                    video
+                        .playback_speed
+                        .unwrap_or(app.config.select.playback_speed),
+                    i64::try_from(video.watch_progress.as_secs())
+                        .expect("This should not overflow"),
+                );
+
+                mpv.command(
+                    "loadfile",
+                    &[
+                        cache_path.to_str().with_context(|| {
+                            format!(
+                                "Failed to parse the video cache path ('{}') as valid utf8",
+                                cache_path.display()
+                            )
+                        })?,
+                        "append-play",
+                        "-1", // Not used for `append-play`, but needed for the next args to take effect.
+                        options.as_str(),
+                    ],
+                )?;
+
+                if *is_focused {
+                    debug!("MpvReload: Setting playlist position to {index}");
+                    mpv.set_property("playlist-pos", index.to_string().as_str())?;
+                }
+
+                Ok::<(), anyhow::Error>(())
+            })?;
+
+        Ok(())
+    }
+
+    pub(crate) fn save_current_watch_progress(
+        &mut self,
+        mpv: &Mpv,
+        ops: &mut Operations<Operation>,
+    ) {
+        let (index, _) = self
+            .get_focused_mut()
+            .expect("This should be some at this point");
+
+        self.save_watch_progress(mpv, index, ops);
+    }
+
+    /// Saves the `watch_progress` of a video at the index.
+    pub(crate) fn save_watch_progress(
+        &mut self,
+        mpv: &Mpv,
+        at: PlaylistIndex,
+        ops: &mut Operations<Operation>,
+    ) {
+        let current_video = self
+            .get_mut(at)
+            .expect("We should never produce invalid playlist indices");
+
+        let watch_progress = match mpv.get_property::<i64>("time-pos") {
+            Ok(time) => u64::try_from(time)
+                .expect("This conversion should never fail as the `time-pos` property is positive"),
+            Err(err) => {
+                // We cannot hard error here, as we would open us to an race condition between mpv
+                // changing the current video and we saving it.
+                trace!(
+                    "While trying to save the watch progress for the current video: \
+                    Failed to get the watchprogress of the currently playling video: \
+                    (This is probably expected, nevertheless showing the raw error) \
+                    {err}"
+                );
+
+                return;
+            }
+        };
+
+        let watch_progress = Duration::from_secs(watch_progress);
+
+        debug!(
+            "Setting the watch progress for the current_video '{}' to {}s",
+            current_video.title_fmt_no_color(),
+            watch_progress.as_secs(),
+        );
+
+        current_video.set_watch_progress(watch_progress, ops);
+    }
+}
diff --git a/crates/yt/src/storage/db/insert/subscription.rs b/crates/yt/src/storage/db/insert/subscription.rs
new file mode 100644
index 0000000..ba9a3e1
--- /dev/null
+++ b/crates/yt/src/storage/db/insert/subscription.rs
@@ -0,0 +1,84 @@
+use crate::storage::db::{
+    insert::{Committable, Operations},
+    subscription::{Subscription, Subscriptions},
+};
+
+use anyhow::Result;
+use sqlx::query;
+
+#[derive(Debug)]
+pub(crate) enum Operation {
+    Add(Subscription),
+    Remove(Subscription),
+}
+
+impl Committable for Operation {
+    async fn commit(self, txn: &mut sqlx::SqliteConnection) -> Result<()> {
+        match self {
+            Operation::Add(subscription) => {
+                let url = subscription.url.as_str();
+
+                query!(
+                    "
+                    INSERT INTO subscriptions (
+                        name,
+                        url
+                    ) VALUES (?, ?);
+                    ",
+                    subscription.name,
+                    url
+                )
+                .execute(txn)
+                .await?;
+
+                println!(
+                    "Subscribed to '{}' at '{}'",
+                    subscription.name, subscription.url
+                );
+                Ok(())
+            }
+            Operation::Remove(subscription) => {
+                let output = query!(
+                    "
+                    DELETE FROM subscriptions
+                    WHERE name = ?
+                    ",
+                    subscription.name,
+                )
+                .execute(txn)
+                .await?;
+
+                assert_eq!(
+                    output.rows_affected(),
+                    1,
+                    "The removed subscription query did effect more (or less) than one row. This is a bug."
+                );
+
+                println!(
+                    "Unsubscribed from '{}' at '{}'",
+                    subscription.name, subscription.url
+                );
+
+                Ok(())
+            }
+        }
+    }
+}
+
+impl Subscription {
+    pub(crate) fn add(self, ops: &mut Operations<Operation>) {
+        ops.push(Operation::Add(self));
+    }
+
+    pub(crate) fn remove(self, ops: &mut Operations<Operation>) {
+        ops.push(Operation::Remove(self));
+    }
+}
+
+impl Subscriptions {
+    pub(crate) fn remove(self, ops: &mut Operations<Operation>) {
+        for sub in self.0.into_values() {
+            ops.push(Operation::Remove(sub));
+        }
+    }
+}
diff --git a/crates/yt/src/storage/db/insert/video/mod.rs b/crates/yt/src/storage/db/insert/video/mod.rs
new file mode 100644
index 0000000..b57f043
--- /dev/null
+++ b/crates/yt/src/storage/db/insert/video/mod.rs
@@ -0,0 +1,599 @@
+use std::{
+    path::{Path, PathBuf},
+    time,
+};
+
+use crate::storage::db::{
+    extractor_hash::ExtractorHash,
+    insert::{Committable, Operations},
+    video::{Priority, Video, VideoStatus, VideoStatusMarker},
+};
+
+use anyhow::{Context, Result};
+use chrono::Utc;
+use log::debug;
+use sqlx::query;
+use tokio::fs;
+
+use super::super::video::TimeStamp;
+
+const fn is_focused_to_value(is_focused: bool) -> Option<i8> {
+    if is_focused { Some(1) } else { None }
+}
+
+#[derive(Debug)]
+pub(crate) enum Operation {
+    Add {
+        description: Option<String>,
+        title: String,
+        parent_subscription_name: Option<String>,
+        thumbnail_url: Option<String>,
+        url: String,
+        extractor_hash: String,
+        status: i64,
+        cache_path: Option<String>,
+        is_focused: Option<i8>,
+        duration: Option<f64>,
+        last_status_change: i64,
+        publish_date: Option<i64>,
+        watch_progress: i64,
+    },
+    // TODO(@bpeetz): Could both the {`Set`,`Remove`}`DownloadPath` ops, be merged into SetStatus
+    // {`Cached`,`Watch`}? <2025-07-14>
+    SetDownloadPath {
+        video: ExtractorHash,
+        path: PathBuf,
+    },
+    RemoveDownloadPath {
+        video: ExtractorHash,
+    },
+    SetStatus {
+        video: ExtractorHash,
+        status: VideoStatus,
+    },
+    SetPriority {
+        video: ExtractorHash,
+        priority: Priority,
+    },
+    SetPlaybackSpeed {
+        video: ExtractorHash,
+        playback_speed: f64,
+    },
+    SetSubtitleLangs {
+        video: ExtractorHash,
+        subtitle_langs: String,
+    },
+    SetWatchProgress {
+        video: ExtractorHash,
+        watch_progress: time::Duration,
+    },
+    SetIsFocused {
+        video: ExtractorHash,
+        is_focused: bool,
+    },
+}
+
+impl Committable for Operation {
+    #[allow(clippy::too_many_lines)]
+    async fn commit(self, txn: &mut sqlx::SqliteConnection) -> Result<()> {
+        match self {
+            Operation::SetDownloadPath { video, path } => {
+                debug!("Setting cache path from '{video}' to '{}'", path.display());
+
+                let path_str = path.display().to_string();
+                let extractor_hash = video.hash().to_string();
+                let status = VideoStatusMarker::Cached.as_db_integer();
+
+                query!(
+                    r#"
+                UPDATE videos
+                SET cache_path = ?, status = ?
+                WHERE extractor_hash = ?;
+                "#,
+                    path_str,
+                    status,
+                    extractor_hash
+                )
+                .execute(txn)
+                .await?;
+
+                Ok(())
+            }
+            Operation::RemoveDownloadPath { video } => {
+                let extractor_hash = video.hash().to_string();
+                let status = VideoStatus::Watch.as_marker().as_db_integer();
+
+                let old = video.get(&mut *txn).await?;
+
+                debug!("Deleting download path of '{video}' ({}).", old.title);
+
+                if let VideoStatus::Cached { cache_path, .. } = &old.status {
+                    if let Ok(true) = cache_path.try_exists() {
+                        fs::remove_file(cache_path).await?;
+                    }
+
+                    {
+                        let info_json_path = old.info_json_path()?.expect("Is downloaded");
+
+                        if let Ok(true) = info_json_path.try_exists() {
+                            fs::remove_file(info_json_path).await?;
+                        }
+                    }
+
+                    {
+                        if old.subtitle_langs.is_some() {
+                            // TODO(@bpeetz): Also clean-up the downloaded subtitle files. <2025-07-05>
+                        }
+                    }
+                } else {
+                    unreachable!(
+                        "A video cannot have a download path deletion \
+                        queued without being marked as Cached."
+                    );
+                }
+
+                query!(
+                    r#"
+                    UPDATE videos
+                    SET cache_path = NULL, status = ?, is_focused = ?
+                    WHERE extractor_hash = ?;
+                    "#,
+                    status,
+                    None::<i32>,
+                    extractor_hash
+                )
+                .execute(txn)
+                .await?;
+
+                Ok(())
+            }
+            Operation::SetStatus { video, status } => {
+                let extractor_hash = video.hash().to_string();
+
+                let old = video.get(&mut *txn).await?;
+
+                let old_marker = old.status.as_marker();
+
+                let (cache_path, is_focused) = {
+                    fn cache_path_to_string(path: &Path) -> Result<String> {
+                        Ok(path
+                            .to_str()
+                            .with_context(|| {
+                                format!(
+                                    "Failed to parse cache path ('{}') as utf8 string",
+                                    path.display()
+                                )
+                            })?
+                            .to_owned())
+                    }
+
+                    if let VideoStatus::Cached {
+                        cache_path,
+                        is_focused,
+                    } = &status
+                    {
+                        (
+                            Some(cache_path_to_string(cache_path)?),
+                            is_focused_to_value(*is_focused),
+                        )
+                    } else {
+                        (None, None)
+                    }
+                };
+
+                let new_status = status.as_marker();
+
+                assert_ne!(
+                    old_marker, new_status,
+                    "We should have never generated this operation"
+                );
+
+                let now = Utc::now().timestamp();
+
+                debug!(
+                    "Changing status of video ('{}' {extractor_hash}) \
+                        from {old_marker:#?} to {new_status:#?}.",
+                    old.title
+                );
+
+                let new_status = new_status.as_db_integer();
+                query!(
+                    r#"
+                    UPDATE videos
+                    SET status = ?, last_status_change = ?, cache_path = ?, is_focused = ?
+                    WHERE extractor_hash = ?;
+                    "#,
+                    new_status,
+                    now,
+                    cache_path,
+                    is_focused,
+                    extractor_hash,
+                )
+                .execute(txn)
+                .await?;
+
+                Ok(())
+            }
+            Operation::SetPriority { video, priority } => {
+                let extractor_hash = video.hash().to_string();
+
+                let new_priority = priority.as_db_integer();
+                query!(
+                    r#"
+                    UPDATE videos
+                    SET priority = ?
+                    WHERE extractor_hash = ?;
+                    "#,
+                    new_priority,
+                    extractor_hash
+                )
+                .execute(txn)
+                .await?;
+
+                Ok(())
+            }
+            Operation::SetWatchProgress {
+                video,
+                watch_progress,
+            } => {
+                let video_extractor_hash = video.hash().to_string();
+                let watch_progress = i64::try_from(watch_progress.as_secs())
+                    .expect("This should never exceed its bounds");
+
+                query!(
+                    r#"
+                    UPDATE videos
+                    SET watch_progress = ?
+                    WHERE extractor_hash = ?;
+                    "#,
+                    watch_progress,
+                    video_extractor_hash,
+                )
+                .execute(txn)
+                .await?;
+
+                Ok(())
+            }
+            Operation::SetPlaybackSpeed {
+                video,
+                playback_speed,
+            } => {
+                let extractor_hash = video.hash().to_string();
+
+                query!(
+                    r#"
+                    UPDATE videos
+                    SET playback_speed = ?
+                    WHERE extractor_hash = ?;
+                    "#,
+                    playback_speed,
+                    extractor_hash
+                )
+                .execute(txn)
+                .await?;
+
+                Ok(())
+            }
+            Operation::SetSubtitleLangs {
+                video,
+                subtitle_langs,
+            } => {
+                let extractor_hash = video.hash().to_string();
+
+                query!(
+                    r#"
+                    UPDATE videos
+                    SET subtitle_langs = ?
+                    WHERE extractor_hash = ?;
+                    "#,
+                    subtitle_langs,
+                    extractor_hash
+                )
+                .execute(txn)
+                .await?;
+
+                Ok(())
+            }
+            Operation::SetIsFocused { video, is_focused } => {
+                debug!("Set is_focused of video: '{video}' to {is_focused}");
+                let new_hash = video.hash().to_string();
+                let new_is_focused = is_focused_to_value(is_focused);
+
+                query!(
+                    r#"
+                    UPDATE videos
+                    SET is_focused = ?
+                    WHERE extractor_hash = ?;
+                    "#,
+                    new_is_focused,
+                    new_hash,
+                )
+                .execute(txn)
+                .await?;
+
+                Ok(())
+            }
+            Operation::Add {
+                parent_subscription_name,
+                thumbnail_url,
+                url,
+                extractor_hash,
+                status,
+                cache_path,
+                is_focused,
+                duration,
+                last_status_change,
+                publish_date,
+                watch_progress,
+                description,
+                title,
+            } => {
+                query!(
+                    r#"
+                    INSERT INTO videos (
+                        description,
+                        duration,
+                        extractor_hash,
+                        is_focused,
+                        last_status_change,
+                        parent_subscription_name,
+                        publish_date,
+                        status,
+                        thumbnail_url,
+                        title,
+                        url,
+                        watch_progress,
+                        cache_path
+                        )
+                    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
+                    "#,
+                    description,
+                    duration,
+                    extractor_hash,
+                    is_focused,
+                    last_status_change,
+                    parent_subscription_name,
+                    publish_date,
+                    status,
+                    thumbnail_url,
+                    title,
+                    url,
+                    watch_progress,
+                    cache_path,
+                )
+                .execute(txn)
+                .await?;
+
+                Ok(())
+            }
+        }
+    }
+}
+
+impl Video {
+    /// Add this in-memory video to the db.
+    pub(crate) fn add(self, ops: &mut Operations<Operation>) -> Result<Self> {
+        let description = self.description.clone();
+        let title = self.title.clone();
+        let parent_subscription_name = self.parent_subscription_name.clone();
+
+        let thumbnail_url = self.thumbnail_url.as_ref().map(ToString::to_string);
+
+        let url = self.url.to_string();
+        let extractor_hash = self.extractor_hash.hash().to_string();
+
+        let status = self.status.as_marker().as_db_integer();
+        let (cache_path, is_focused) = if let VideoStatus::Cached {
+            cache_path,
+            is_focused,
+        } = &self.status
+        {
+            (
+                Some(
+                    cache_path
+                        .to_str()
+                        .with_context(|| {
+                            format!(
+                                "Failed to prase cache path '{}' as utf-8 string",
+                                cache_path.display()
+                            )
+                        })?
+                        .to_string(),
+                ),
+                is_focused_to_value(*is_focused),
+            )
+        } else {
+            (None, None)
+        };
+
+        let duration: Option<f64> = self.duration.as_secs_f64();
+        let last_status_change: i64 = self.last_status_change.as_secs();
+        let publish_date: Option<i64> = self.publish_date.map(TimeStamp::as_secs);
+        let watch_progress: i64 =
+            i64::try_from(self.watch_progress.as_secs()).expect("This should never exceed a u32");
+
+        ops.push(Operation::Add {
+            description,
+            title,
+            parent_subscription_name,
+            thumbnail_url,
+            url,
+            extractor_hash,
+            status,
+            cache_path,
+            is_focused,
+            duration,
+            last_status_change,
+            publish_date,
+            watch_progress,
+        });
+
+        Ok(self)
+    }
+
+    /// Set the download path of a video.
+    ///
+    /// # Note
+    /// This will also set the status to `Cached`.
+    pub(crate) fn set_download_path(&mut self, path: &Path, ops: &mut Operations<Operation>) {
+        if let VideoStatus::Cached { cache_path, .. } = &mut self.status {
+            if cache_path != path {
+                // Update the in-memory video.
+                path.clone_into(cache_path);
+
+                ops.push(Operation::SetDownloadPath {
+                    video: self.extractor_hash,
+                    path: path.to_owned(),
+                });
+            }
+        } else {
+            self.status = VideoStatus::Cached {
+                cache_path: path.to_owned(),
+                is_focused: false,
+            };
+
+            ops.push(Operation::SetDownloadPath {
+                video: self.extractor_hash,
+                path: path.to_owned(),
+            });
+        }
+    }
+
+    /// Remove the download path of a video.
+    ///
+    /// # Note
+    /// This will also set the status to `Watch`.
+    ///
+    /// # Panics
+    /// If the status is not `Cached`.
+    pub(crate) fn remove_download_path(&mut self, ops: &mut Operations<Operation>) {
+        if let VideoStatus::Cached { .. } = &mut self.status {
+            self.status = VideoStatus::Watch;
+            ops.push(Operation::RemoveDownloadPath {
+                video: self.extractor_hash,
+            });
+        } else {
+            unreachable!("Can only remove the path from a `Cached` video");
+        }
+    }
+
+    /// Update the `is_focused` flag of this video.
+    ///
+    /// # Note
+    /// It will only actually add operations, if the `is_focused` flag is different.
+    ///
+    /// # Panics
+    /// If the status is not `Cached`.
+    pub(crate) fn set_focused(&mut self, new_is_focused: bool, ops: &mut Operations<Operation>) {
+        if let VideoStatus::Cached { is_focused, .. } = &mut self.status {
+            if *is_focused != new_is_focused {
+                *is_focused = new_is_focused;
+
+                ops.push(Operation::SetIsFocused {
+                    video: self.extractor_hash,
+                    is_focused: new_is_focused,
+                });
+            }
+        } else {
+            unreachable!("Can only change `is_focused` on a Cached video.");
+        }
+    }
+
+    /// Set the status of this video.
+    ///
+    /// # Note
+    /// This will not actually add any operations, if the new status equals the old one.
+    pub(crate) fn set_status(&mut self, status: VideoStatus, ops: &mut Operations<Operation>) {
+        if self.status != status {
+            status.clone_into(&mut self.status);
+
+            ops.push(Operation::SetStatus {
+                video: self.extractor_hash,
+                status,
+            });
+        }
+    }
+
+    /// Set the priority of this video.
+    ///
+    /// # Note
+    /// This will not actually add any operations, if the new priority equals the old one.
+    pub(crate) fn set_priority(&mut self, priority: Priority, ops: &mut Operations<Operation>) {
+        if self.priority != priority {
+            self.priority = priority;
+
+            ops.push(Operation::SetPriority {
+                video: self.extractor_hash,
+                priority,
+            });
+        }
+    }
+
+    /// Set the watch progress.
+    ///
+    /// # Note
+    /// This will not actually add any operations,
+    /// if the new watch progress equals the old one.
+    pub(crate) fn set_watch_progress(
+        &mut self,
+        watch_progress: time::Duration,
+        ops: &mut Operations<Operation>,
+    ) {
+        if self.watch_progress != watch_progress {
+            self.watch_progress = watch_progress;
+
+            ops.push(Operation::SetWatchProgress {
+                video: self.extractor_hash,
+                watch_progress,
+            });
+        }
+    }
+
+    /// Set the playback speed of this video.
+    ///
+    /// # Note
+    /// This will not actually add any operations, if the new speed equals the old one.
+    pub(crate) fn set_playback_speed(
+        &mut self,
+        playback_speed: f64,
+        ops: &mut Operations<Operation>,
+    ) {
+        if self.playback_speed != Some(playback_speed) {
+            self.playback_speed = Some(playback_speed);
+
+            ops.push(Operation::SetPlaybackSpeed {
+                video: self.extractor_hash,
+                playback_speed,
+            });
+        }
+    }
+
+    /// Set the subtitle langs of this video.
+    ///
+    /// # Note
+    /// This will not actually add any operations, if the new langs equal the old one.
+    pub(crate) fn set_subtitle_langs(
+        &mut self,
+        subtitle_langs: String,
+        ops: &mut Operations<Operation>,
+    ) {
+        if self.subtitle_langs.as_ref() != Some(&subtitle_langs) {
+            self.subtitle_langs = Some(subtitle_langs.clone());
+
+            ops.push(Operation::SetSubtitleLangs {
+                video: self.extractor_hash,
+                subtitle_langs,
+            });
+        }
+    }
+
+    /// Mark this video watched.
+    /// This will both set the status to `Watched` and the `cache_path` to Null.
+    ///
+    /// # Panics
+    /// Only if assertions fail.
+    pub(crate) fn set_watched(&mut self, ops: &mut Operations<Operation>) {
+        self.remove_download_path(ops);
+        self.set_status(VideoStatus::Watched, ops);
+    }
+}
diff --git a/crates/yt/src/storage/db/mod.rs b/crates/yt/src/storage/db/mod.rs
new file mode 100644
index 0000000..c0e16b0
--- /dev/null
+++ b/crates/yt/src/storage/db/mod.rs
@@ -0,0 +1,7 @@
+pub(crate) mod get;
+pub(crate) mod insert;
+
+pub(crate) mod extractor_hash;
+pub(crate) mod subscription;
+pub(crate) mod video;
+pub(crate) mod playlist;
diff --git a/crates/yt/src/storage/db/playlist/mod.rs b/crates/yt/src/storage/db/playlist/mod.rs
new file mode 100644
index 0000000..6fca98a
--- /dev/null
+++ b/crates/yt/src/storage/db/playlist/mod.rs
@@ -0,0 +1,49 @@
+use std::ops::Add;
+
+use crate::storage::db::video::Video;
+
+/// Zero-based index into the internal playlist.
+#[derive(Debug, Clone, Copy)]
+pub(crate) struct PlaylistIndex(usize);
+
+impl From<PlaylistIndex> for usize {
+    fn from(value: PlaylistIndex) -> Self {
+        value.0
+    }
+}
+
+impl From<usize> for PlaylistIndex {
+    fn from(value: usize) -> Self {
+        Self(value)
+    }
+}
+
+impl Add<usize> for PlaylistIndex {
+    type Output = Self;
+
+    fn add(self, rhs: usize) -> Self::Output {
+        Self(self.0 + rhs)
+    }
+}
+
+impl Add for PlaylistIndex {
+    type Output = Self;
+
+    fn add(self, rhs: Self) -> Self::Output {
+        Self(self.0 + rhs.0)
+    }
+}
+
+/// A representation of the internal Playlist
+#[derive(Debug)]
+pub(crate) struct Playlist {
+    pub(crate) videos: Vec<Video>,
+}
+
+impl Playlist {
+    /// Returns the number of videos in the playlist
+    #[must_use]
+    pub(crate) fn len(&self) -> usize {
+        self.videos.len()
+    }
+}
diff --git a/crates/yt/src/storage/db/subscription.rs b/crates/yt/src/storage/db/subscription.rs
new file mode 100644
index 0000000..07e5eec
--- /dev/null
+++ b/crates/yt/src/storage/db/subscription.rs
@@ -0,0 +1,41 @@
+use std::collections::HashMap;
+
+use anyhow::Result;
+use log::debug;
+use url::Url;
+use yt_dlp::{json_cast, options::YoutubeDLOptions};
+
+#[derive(Clone, Debug)]
+pub(crate) struct Subscription {
+    /// The human readable name of this subscription
+    pub(crate) name: String,
+
+    /// The URL this subscription subscribes to
+    pub(crate) url: Url,
+}
+
+impl Subscription {
+    #[must_use]
+    pub(crate) fn new(name: String, url: Url) -> Self {
+        Self { name, url }
+    }
+}
+
+#[derive(Default, Debug)]
+pub(crate) struct Subscriptions(pub(crate) HashMap<String, Subscription>);
+
+/// Check whether an URL could be used as a subscription URL
+pub(crate) async fn check_url(url: Url) -> Result<bool> {
+    let yt_dlp = YoutubeDLOptions::new()
+        .set("playliststart", 1)
+        .set("playlistend", 10)
+        .set("noplaylist", false)
+        .set("extract_flat", "in_playlist")
+        .build()?;
+
+    let info = yt_dlp.extract_info(&url, false, false)?;
+
+    debug!("{info:#?}");
+
+    Ok(info.get("_type").map(|v| json_cast!(v, as_str)) == Some("playlist"))
+}
diff --git a/crates/yt/src/storage/video_database/mod.rs b/crates/yt/src/storage/db/video.rs
index 74d09f0..f1b8bb9 100644
--- a/crates/yt/src/storage/video_database/mod.rs
+++ b/crates/yt/src/storage/db/video.rs
@@ -1,63 +1,104 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::{
-    fmt::{Display, Write},
-    path::PathBuf,
-    time::Duration,
-};
+use std::{fmt::Display, path::PathBuf, time::Duration};
 
 use chrono::{DateTime, Utc};
 use url::Url;
 
 use crate::{
-    app::App, select::selection_file::duration::MaybeDuration,
-    storage::video_database::extractor_hash::ExtractorHash,
+    select::selection_file::duration::MaybeDuration, storage::db::extractor_hash::ExtractorHash,
 };
 
-pub mod downloader;
-pub mod extractor_hash;
-pub mod get;
-pub mod notify;
-pub mod set;
+macro_rules! video_from_record {
+    ($record:expr) => {
+        $crate::storage::db::video::Video {
+            description: $record.description.clone(),
+            duration: $crate::select::selection_file::duration::MaybeDuration::from_maybe_secs_f64(
+                $record.duration,
+            ),
+            extractor_hash: $crate::storage::db::extractor_hash::ExtractorHash::from_hash(
+                $record
+                    .extractor_hash
+                    .parse()
+                    .expect("The db hash should be a valid blake3 hash"),
+            ),
+            last_status_change: $crate::storage::db::video::TimeStamp::from_secs(
+                $record.last_status_change,
+            ),
+            parent_subscription_name: $record.parent_subscription_name.clone(),
+            publish_date: $record
+                .publish_date
+                .map(|pd| $crate::storage::db::video::TimeStamp::from_secs(pd)),
+            status: {
+                let marker =
+                    $crate::storage::db::video::VideoStatusMarker::from_db_integer($record.status);
+                let optional = if let Some(cache_path) = &$record.cache_path {
+                    Some((
+                        std::path::PathBuf::from(cache_path),
+                        if $record.is_focused == Some(1) {
+                            true
+                        } else {
+                            false
+                        },
+                    ))
+                } else {
+                    None
+                };
+                $crate::storage::db::video::VideoStatus::from_marker(marker, optional)
+            },
+            thumbnail_url: if let Some(url) = &$record.thumbnail_url {
+                Some(url::Url::parse(url).expect("Parsing this as url should always work"))
+            } else {
+                None
+            },
+            title: $record.title.clone(),
+            url: url::Url::parse(&$record.url).expect("Parsing this as url should always work"),
+            priority: $crate::storage::db::video::Priority::from($record.priority),
+            watch_progress: std::time::Duration::from_secs(
+                u64::try_from($record.watch_progress).expect("The record is positive i64"),
+            ),
+            subtitle_langs: $record.subtitle_langs.clone(),
+            playback_speed: $record.playback_speed,
+        }
+    };
+}
+pub(crate) use video_from_record;
 
 #[derive(Debug, Clone)]
-pub struct Video {
-    pub description: Option<String>,
-    pub duration: MaybeDuration,
-    pub extractor_hash: ExtractorHash,
-    pub last_status_change: TimeStamp,
+pub(crate) struct Video {
+    pub(crate) description: Option<String>,
+    pub(crate) duration: MaybeDuration,
+    pub(crate) extractor_hash: ExtractorHash,
+    pub(crate) last_status_change: TimeStamp,
 
     /// The associated subscription this video was fetched from (null, when the video was `add`ed)
-    pub parent_subscription_name: Option<String>,
-    pub priority: Priority,
-    pub publish_date: Option<TimeStamp>,
-    pub status: VideoStatus,
-    pub thumbnail_url: Option<Url>,
-    pub title: String,
-    pub url: Url,
+    pub(crate) parent_subscription_name: Option<String>,
+    pub(crate) priority: Priority,
+    pub(crate) publish_date: Option<TimeStamp>,
+    pub(crate) status: VideoStatus,
+    pub(crate) thumbnail_url: Option<Url>,
+    pub(crate) title: String,
+    pub(crate) url: Url,
 
     /// The seconds the user has already watched the video
-    pub watch_progress: Duration,
+    pub(crate) watch_progress: Duration,
+
+    /// Which subtitles to include, when downloading this video.
+    /// In the form of `lang1,lang2,lang3` (e.g. `en,de,sv`)
+    pub(crate) subtitle_langs: Option<String>,
+
+    /// The playback speed to use, when watching this video.
+    /// Value is in percent, so 1 is 100%, 2.7 is 270%, and so on.
+    pub(crate) playback_speed: Option<f64>,
 }
 
 /// The priority of a [`Video`].
 #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
-pub struct Priority {
+pub(crate) struct Priority {
     value: i64,
 }
 impl Priority {
     /// Return the underlying value to insert that into the database
     #[must_use]
-    pub fn as_db_integer(&self) -> i64 {
+    pub(crate) fn as_db_integer(self) -> i64 {
         self.value
     }
 }
@@ -74,25 +115,25 @@ impl Display for Priority {
 
 /// An UNIX time stamp.
 #[derive(Debug, Default, Clone, Copy)]
-pub struct TimeStamp {
+pub(crate) struct TimeStamp {
     value: i64,
 }
 impl TimeStamp {
     /// Return the seconds since the UNIX epoch for this [`TimeStamp`].
     #[must_use]
-    pub fn as_secs(&self) -> i64 {
+    pub(crate) fn as_secs(self) -> i64 {
         self.value
     }
 
     /// Construct a [`TimeStamp`] from a count of seconds since the UNIX epoch.
     #[must_use]
-    pub fn from_secs(value: i64) -> Self {
+    pub(crate) fn from_secs(value: i64) -> Self {
         Self { value }
     }
 
     /// Construct a [`TimeStamp`] from the current time.
     #[must_use]
-    pub fn from_now() -> Self {
+    pub(crate) fn from_now() -> Self {
         Self {
             value: Utc::now().timestamp(),
         }
@@ -107,49 +148,6 @@ impl Display for TimeStamp {
     }
 }
 
-#[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.
-    #[must_use]
-    pub fn to_cli_flags(self, app: &App) -> String {
-        let mut f = String::new();
-
-        if (self.mpv.playback_speed - app.config.select.playback_speed).abs() > f64::EPSILON {
-            write!(f, " --speed '{}'", self.mpv.playback_speed).expect("Works");
-        }
-        if self.yt_dlp.subtitle_langs != app.config.select.subtitle_langs {
-            write!(f, " --subtitle-langs '{}'", self.yt_dlp.subtitle_langs).expect("Works");
-        }
-
-        f.trim().to_owned()
-    }
-}
-
-#[derive(Debug, Clone, Copy)]
-/// 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>
 ///     /    \
@@ -159,7 +157,7 @@ pub struct YtDlpOptions {
 ///     |
 /// Watched                     // yt watch
 #[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
-pub enum VideoStatus {
+pub(crate) enum VideoStatus {
     #[default]
     Pick,
 
@@ -186,7 +184,10 @@ impl VideoStatus {
     /// # Panics
     /// Only if internal expectations fail.
     #[must_use]
-    pub fn from_marker(marker: VideoStatusMarker, optional: Option<(PathBuf, bool)>) -> Self {
+    pub(crate) fn from_marker(
+        marker: VideoStatusMarker,
+        optional: Option<(PathBuf, bool)>,
+    ) -> Self {
         match marker {
             VideoStatusMarker::Pick => Self::Pick,
             VideoStatusMarker::Watch => Self::Watch,
@@ -204,26 +205,9 @@ impl VideoStatus {
         }
     }
 
-    /// Turn the [`VideoStatus`] to its internal parts. This is only really useful for the database
-    /// functions.
-    #[must_use]
-    pub fn to_parts_for_db(self) -> (VideoStatusMarker, Option<(PathBuf, bool)>) {
-        match self {
-            VideoStatus::Pick => (VideoStatusMarker::Pick, None),
-            VideoStatus::Watch => (VideoStatusMarker::Watch, None),
-            VideoStatus::Cached {
-                cache_path,
-                is_focused,
-            } => (VideoStatusMarker::Cached, Some((cache_path, is_focused))),
-            VideoStatus::Watched => (VideoStatusMarker::Watched, None),
-            VideoStatus::Drop => (VideoStatusMarker::Drop, None),
-            VideoStatus::Dropped => (VideoStatusMarker::Dropped, None),
-        }
-    }
-
     /// Return the associated [`VideoStatusMarker`] for this [`VideoStatus`].
     #[must_use]
-    pub fn as_marker(&self) -> VideoStatusMarker {
+    pub(crate) fn as_marker(&self) -> VideoStatusMarker {
         match self {
             VideoStatus::Pick => VideoStatusMarker::Pick,
             VideoStatus::Watch => VideoStatusMarker::Watch,
@@ -237,7 +221,7 @@ impl VideoStatus {
 
 /// Unit only variant of [`VideoStatus`]
 #[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
-pub enum VideoStatusMarker {
+pub(crate) enum VideoStatusMarker {
     #[default]
     Pick,
 
@@ -255,7 +239,7 @@ pub enum VideoStatusMarker {
 }
 
 impl VideoStatusMarker {
-    pub const ALL: &'static [Self; 6] = &[
+    pub(crate) const ALL: &'static [Self; 6] = &[
         Self::Pick,
         //
         Self::Watch,
@@ -267,7 +251,7 @@ impl VideoStatusMarker {
     ];
 
     #[must_use]
-    pub fn as_command(&self) -> &str {
+    pub(crate) fn as_command(&self) -> &str {
         // NOTE: Keep the serialize able variants synced with the main `select` function <2024-06-14>
         // Also try to ensure, that the strings have the same length
         match self {
@@ -281,7 +265,7 @@ impl VideoStatusMarker {
     }
 
     #[must_use]
-    pub fn as_db_integer(&self) -> i64 {
+    pub(crate) fn as_db_integer(self) -> i64 {
         // These numbers should not change their mapping!
         // Oh, and keep them in sync with the SQLite check constraint.
         match self {
@@ -296,7 +280,7 @@ impl VideoStatusMarker {
         }
     }
     #[must_use]
-    pub fn from_db_integer(num: i64) -> Self {
+    pub(crate) fn from_db_integer(num: i64) -> Self {
         match num {
             0 => Self::Pick,
 
@@ -314,7 +298,7 @@ impl VideoStatusMarker {
     }
 
     #[must_use]
-    pub fn as_str(&self) -> &'static str {
+    pub(crate) fn as_str(self) -> &'static str {
         match self {
             Self::Pick => "Pick",
 
diff --git a/crates/yt/src/storage/video_database/notify.rs b/crates/yt/src/storage/notify.rs
index b55c00a..e0ee4e9 100644
--- a/crates/yt/src/storage/video_database/notify.rs
+++ b/crates/yt/src/storage/notify.rs
@@ -26,7 +26,7 @@ use tokio::task;
 
 /// This functions registers a watcher for the database and only returns once a write was
 /// registered for the database.
-pub async fn wait_for_db_write(app: &App) -> Result<()> {
+pub(crate) async fn wait_for_db_write(app: &App) -> Result<()> {
     let db_path: PathBuf = app.config.paths.database_path.clone();
     task::spawn_blocking(move || wait_for_db_write_sync(&db_path)).await?
 }
@@ -53,7 +53,7 @@ fn wait_for_db_write_sync(db_path: &Path) -> Result<()> {
 }
 
 /// This functions registers a watcher for the cache path and returns once a file was removed
-pub async fn wait_for_cache_reduction(app: &App) -> Result<()> {
+pub(crate) async fn wait_for_cache_reduction(app: &App) -> Result<()> {
     let download_directory: PathBuf = app.config.paths.download_dir.clone();
     task::spawn_blocking(move || wait_for_cache_reduction_sync(&download_directory)).await?
 }
diff --git a/crates/yt/src/storage/subscriptions.rs b/crates/yt/src/storage/subscriptions.rs
deleted file mode 100644
index 0e8ae85..0000000
--- a/crates/yt/src/storage/subscriptions.rs
+++ /dev/null
@@ -1,141 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-//! Handle subscriptions
-
-use std::collections::HashMap;
-
-use anyhow::Result;
-use log::debug;
-use sqlx::query;
-use url::Url;
-use yt_dlp::{json_cast, options::YoutubeDLOptions};
-
-use crate::{app::App, unreachable::Unreachable};
-
-#[derive(Clone, Debug)]
-pub struct Subscription {
-    /// The human readable name of this subscription
-    pub name: String,
-
-    /// The URL this subscription subscribes to
-    pub url: Url,
-}
-
-impl Subscription {
-    #[must_use]
-    pub fn new(name: String, url: Url) -> Self {
-        Self { name, url }
-    }
-}
-
-/// Check whether an URL could be used as a subscription URL
-pub async fn check_url(url: Url) -> Result<bool> {
-    let yt_dlp = YoutubeDLOptions::new()
-        .set("playliststart", 1)
-        .set("playlistend", 10)
-        .set("noplaylist", false)
-        .set("extract_flat", "in_playlist")
-        .build()?;
-
-    let info = yt_dlp.extract_info(&url, false, false)?;
-
-    debug!("{info:#?}");
-
-    Ok(info.get("_type").map(|v| json_cast!(v, as_str)) == Some("playlist"))
-}
-
-#[derive(Default, Debug)]
-pub struct Subscriptions(pub(crate) HashMap<String, Subscription>);
-
-/// Remove all subscriptions
-pub async fn remove_all(app: &App) -> Result<()> {
-    query!(
-        "
-        DELETE FROM subscriptions;
-    ",
-    )
-    .execute(&app.database)
-    .await?;
-
-    Ok(())
-}
-
-/// Get a list of subscriptions
-pub async fn get(app: &App) -> Result<Subscriptions> {
-    let raw_subs = query!(
-        "
-        SELECT *
-        FROM subscriptions;
-    "
-    )
-    .fetch_all(&app.database)
-    .await?;
-
-    let subscriptions: HashMap<String, Subscription> = raw_subs
-        .into_iter()
-        .map(|sub| {
-            (
-                sub.name.clone(),
-                Subscription::new(
-                    sub.name,
-                    Url::parse(&sub.url).unreachable("It was an URL, when we inserted it."),
-                ),
-            )
-        })
-        .collect();
-
-    Ok(Subscriptions(subscriptions))
-}
-
-pub async fn add_subscription(app: &App, sub: &Subscription) -> Result<()> {
-    let url = sub.url.to_string();
-
-    query!(
-        "
-        INSERT INTO subscriptions (
-            name,
-            url
-        ) VALUES (?, ?);
-    ",
-        sub.name,
-        url
-    )
-    .execute(&app.database)
-    .await?;
-
-    println!("Subscribed to '{}' at '{}'", sub.name, sub.url);
-    Ok(())
-}
-
-/// # Panics
-/// Only if assertions fail
-pub async fn remove_subscription(app: &App, sub: &Subscription) -> Result<()> {
-    let output = query!(
-        "
-        DELETE FROM subscriptions
-        WHERE name = ?
-    ",
-        sub.name,
-    )
-    .execute(&app.database)
-    .await?;
-
-    assert_eq!(
-        output.rows_affected(),
-        1,
-        "The remove subscriptino query did effect more (or less) than one row. This is a bug."
-    );
-
-    println!("Unsubscribed from '{}' at '{}'", sub.name, sub.url);
-
-    Ok(())
-}
diff --git a/crates/yt/src/storage/video_database/downloader.rs b/crates/yt/src/storage/video_database/downloader.rs
deleted file mode 100644
index a95081e..0000000
--- a/crates/yt/src/storage/video_database/downloader.rs
+++ /dev/null
@@ -1,130 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::path::{Path, PathBuf};
-
-use anyhow::Result;
-use log::debug;
-use sqlx::query;
-
-use crate::{
-    app::App,
-    storage::video_database::{VideoStatus, VideoStatusMarker},
-    unreachable::Unreachable,
-    video_from_record,
-};
-
-use super::{ExtractorHash, Video};
-
-/// Returns to next video which should be downloaded. This respects the priority assigned by select.
-/// It does not return videos, which are already cached.
-///
-/// # Panics
-/// Only if assertions fail.
-pub async fn get_next_uncached_video(app: &App) -> Result<Option<Video>> {
-    let status = VideoStatus::Watch.as_marker().as_db_integer();
-
-    // NOTE: The ORDER BY statement should be the same as the one in [`get::videos`].<2024-08-22>
-    let result = query!(
-        r#"
-        SELECT *
-        FROM videos
-        WHERE status = ? AND cache_path IS NULL
-        ORDER BY priority DESC, publish_date DESC
-        LIMIT 1;
-    "#,
-        status
-    )
-    .fetch_one(&app.database)
-    .await;
-
-    if let Err(sqlx::Error::RowNotFound) = result {
-        Ok(None)
-    } else {
-        let base = result?;
-
-        Ok(Some(video_from_record! {base}))
-    }
-}
-
-/// Update the cached path of a video. Will be set to NULL if the path is None
-/// This will also set the status to `Cached` when path is Some, otherwise it set's the status to
-/// `Watch`.
-pub async fn set_video_cache_path(
-    app: &App,
-    video: &ExtractorHash,
-    path: Option<&Path>,
-) -> Result<()> {
-    if let Some(path) = path {
-        debug!(
-            "Setting cache path from '{}' to '{}'",
-            video.into_short_hash(app).await?,
-            path.display()
-        );
-
-        let path_str = path.display().to_string();
-        let extractor_hash = video.hash().to_string();
-        let status = VideoStatusMarker::Cached.as_db_integer();
-
-        query!(
-            r#"
-            UPDATE videos
-            SET cache_path = ?, status = ?
-            WHERE extractor_hash = ?;
-        "#,
-            path_str,
-            status,
-            extractor_hash
-        )
-        .execute(&app.database)
-        .await?;
-
-        Ok(())
-    } else {
-        debug!(
-            "Setting cache path from '{}' to NULL",
-            video.into_short_hash(app).await?,
-        );
-
-        let extractor_hash = video.hash().to_string();
-        let status = VideoStatus::Watch.as_marker().as_db_integer();
-
-        query!(
-            r#"
-            UPDATE videos
-            SET cache_path = NULL, status = ?
-            WHERE extractor_hash = ?;
-        "#,
-            status,
-            extractor_hash
-        )
-        .execute(&app.database)
-        .await?;
-
-        Ok(())
-    }
-}
-
-/// Returns the number of cached videos
-pub async fn get_allocated_cache(app: &App) -> Result<u32> {
-    let count = query!(
-        r#"
-        SELECT COUNT(cache_path) as count
-        FROM videos
-        WHERE cache_path IS NOT NULL;
-"#,
-    )
-    .fetch_one(&app.database)
-    .await?;
-
-    Ok(u32::try_from(count.count)
-        .unreachable("The value should be strictly positive (and bolow `u32::Max`)"))
-}
diff --git a/crates/yt/src/storage/video_database/get/mod.rs b/crates/yt/src/storage/video_database/get/mod.rs
deleted file mode 100644
index e76131e..0000000
--- a/crates/yt/src/storage/video_database/get/mod.rs
+++ /dev/null
@@ -1,307 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-//! These functions interact with the storage db in a read-only way. They are added on-demand (as
-//! you could theoretically just could do everything with the `get_videos` function), as
-//! performance or convince requires.
-use std::{fs::File, path::PathBuf};
-
-use anyhow::{Context, Result, bail};
-use blake3::Hash;
-use log::{debug, trace};
-use sqlx::query;
-use yt_dlp::info_json::InfoJson;
-
-use crate::{
-    app::App,
-    storage::{
-        subscriptions::Subscription,
-        video_database::{Video, extractor_hash::ExtractorHash},
-    },
-    unreachable::Unreachable,
-};
-
-use super::{MpvOptions, VideoOptions, VideoStatus, VideoStatusMarker, YtDlpOptions};
-
-mod playlist;
-pub use playlist::*;
-
-#[macro_export]
-macro_rules! video_from_record {
-    ($record:expr) => {
-        Video {
-            description: $record.description.clone(),
-            duration: $crate::storage::video_database::MaybeDuration::from_maybe_secs_f64(
-                $record.duration,
-            ),
-            extractor_hash:
-                $crate::storage::video_database::extractor_hash::ExtractorHash::from_hash(
-                    $record
-                        .extractor_hash
-                        .parse()
-                        .expect("The db hash should be a valid blake3 hash"),
-                ),
-            last_status_change: $crate::storage::video_database::TimeStamp::from_secs(
-                $record.last_status_change,
-            ),
-            parent_subscription_name: $record.parent_subscription_name.clone(),
-            publish_date: $record
-                .publish_date
-                .map(|pd| $crate::storage::video_database::TimeStamp::from_secs(pd)),
-            status: {
-                let marker = $crate::storage::video_database::VideoStatusMarker::from_db_integer(
-                    $record.status,
-                );
-
-                let optional = if let Some(cache_path) = &$record.cache_path {
-                    Some((
-                        PathBuf::from(cache_path),
-                        if $record.is_focused == Some(1) {
-                            true
-                        } else {
-                            false
-                        },
-                    ))
-                } else {
-                    None
-                };
-
-                $crate::storage::video_database::VideoStatus::from_marker(marker, optional)
-            },
-            thumbnail_url: if let Some(url) = &$record.thumbnail_url {
-                Some(url::Url::parse(&url).expect("Parsing this as url should always work"))
-            } else {
-                None
-            },
-            title: $record.title.clone(),
-            url: url::Url::parse(&$record.url).expect("Parsing this as url should always work"),
-            priority: $crate::storage::video_database::Priority::from($record.priority),
-
-            watch_progress: std::time::Duration::from_secs(
-                u64::try_from($record.watch_progress).expect("The record is positive i64"),
-            ),
-        }
-    };
-}
-
-/// Returns the videos that are in the `allowed_states`.
-///
-/// # Panics
-/// Only, if assertions fail.
-pub async fn videos(app: &App, allowed_states: &[VideoStatusMarker]) -> Result<Vec<Video>> {
-    fn test(all_states: &[VideoStatusMarker], check: VideoStatusMarker) -> Option<i64> {
-        if all_states.contains(&check) {
-            trace!("State '{check:?}' marked as active");
-            Some(check.as_db_integer())
-        } else {
-            trace!("State '{check:?}' marked as inactive");
-            None
-        }
-    }
-    fn states_to_string(allowed_states: &[VideoStatusMarker]) -> String {
-        let mut states = allowed_states
-            .iter()
-            .fold(String::from("&["), |mut acc, state| {
-                acc.push_str(state.as_str());
-                acc.push_str(", ");
-                acc
-            });
-        states = states.trim().to_owned();
-        states = states.trim_end_matches(',').to_owned();
-        states.push(']');
-        states
-    }
-
-    debug!(
-        "Fetching videos in the states: '{}'",
-        states_to_string(allowed_states)
-    );
-    let active_pick: Option<i64> = test(allowed_states, VideoStatusMarker::Pick);
-    let active_watch: Option<i64> = test(allowed_states, VideoStatusMarker::Watch);
-    let active_cached: Option<i64> = test(allowed_states, VideoStatusMarker::Cached);
-    let active_watched: Option<i64> = test(allowed_states, VideoStatusMarker::Watched);
-    let active_drop: Option<i64> = test(allowed_states, VideoStatusMarker::Drop);
-    let active_dropped: Option<i64> = test(allowed_states, VideoStatusMarker::Dropped);
-
-    let videos = query!(
-        r"
-        SELECT *
-          FROM videos
-          WHERE status IN (?,?,?,?,?,?)
-          ORDER BY priority DESC, publish_date DESC;
-          ",
-        active_pick,
-        active_watch,
-        active_cached,
-        active_watched,
-        active_drop,
-        active_dropped,
-    )
-    .fetch_all(&app.database)
-    .await
-    .with_context(|| {
-        format!(
-            "Failed to query videos with states: '{}'",
-            states_to_string(allowed_states)
-        )
-    })?;
-
-    let real_videos: Vec<Video> = videos
-        .iter()
-        .map(|base| -> Video {
-            video_from_record! {base}
-        })
-        .collect();
-
-    Ok(real_videos)
-}
-
-pub fn video_info_json(video: &Video) -> Result<Option<InfoJson>> {
-    if let VideoStatus::Cached { mut cache_path, .. } = video.status.clone() {
-        if !cache_path.set_extension("info.json") {
-            bail!(
-                "Failed to change path extension to 'info.json': {}",
-                cache_path.display()
-            );
-        }
-        let info_json_string = File::open(cache_path)?;
-        let info_json: InfoJson = serde_json::from_reader(&info_json_string)?;
-
-        Ok(Some(info_json))
-    } else {
-        Ok(None)
-    }
-}
-
-pub async fn video_by_hash(app: &App, hash: &ExtractorHash) -> Result<Video> {
-    let ehash = hash.hash().to_string();
-
-    let raw_video = query!(
-        "
-        SELECT * FROM videos WHERE extractor_hash = ?;
-        ",
-        ehash
-    )
-    .fetch_one(&app.database)
-    .await?;
-
-    Ok(video_from_record! {raw_video})
-}
-
-pub async fn get_all_hashes(app: &App) -> Result<Vec<Hash>> {
-    let hashes_hex = query!(
-        r#"
-        SELECT extractor_hash
-        FROM videos;
-    "#
-    )
-    .fetch_all(&app.database)
-    .await?;
-
-    Ok(hashes_hex
-        .iter()
-        .map(|hash| {
-            Hash::from_hex(&hash.extractor_hash).unreachable(
-                "These values started as blake3 hashes, they should stay blake3 hashes",
-            )
-        })
-        .collect())
-}
-
-pub async fn get_video_hashes(app: &App, subs: &Subscription) -> Result<Vec<Hash>> {
-    let hashes_hex = query!(
-        r#"
-        SELECT extractor_hash
-        FROM videos
-        WHERE parent_subscription_name = ?;
-    "#,
-        subs.name
-    )
-    .fetch_all(&app.database)
-    .await?;
-
-    Ok(hashes_hex
-        .iter()
-        .map(|hash| {
-            Hash::from_hex(&hash.extractor_hash).unreachable(
-                "These values started as blake3 hashes, they should stay blake3 hashes",
-            )
-        })
-        .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
-    .with_context(|| {
-        format!("Failed to fetch the `yt_dlp_video_opts` for video with hash: '{hash}'",)
-    })?;
-
-    Ok(YtDlpOptions {
-        subtitle_langs: yt_dlp_options.subtitle_langs,
-    })
-}
-pub async fn 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
-    .with_context(|| {
-        format!("Failed to fetch the `mpv_video_opts` for video with hash: '{hash}'")
-    })?;
-
-    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
-    .with_context(|| format!("Failed to fetch the `video_opts` for video with hash: '{hash}'"))?;
-
-    let mpv = MpvOptions {
-        playback_speed: opts.playback_speed,
-    };
-    let yt_dlp = YtDlpOptions {
-        subtitle_langs: opts.subtitle_langs,
-    };
-
-    Ok(VideoOptions { yt_dlp, mpv })
-}
diff --git a/crates/yt/src/storage/video_database/get/playlist/iterator.rs b/crates/yt/src/storage/video_database/get/playlist/iterator.rs
deleted file mode 100644
index 4c45bf7..0000000
--- a/crates/yt/src/storage/video_database/get/playlist/iterator.rs
+++ /dev/null
@@ -1,101 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::{
-    collections::VecDeque,
-    path::{Path, PathBuf},
-};
-
-use crate::storage::video_database::{Video, VideoStatus};
-
-use super::Playlist;
-
-/// Turn a cached video into it's `cache_path`
-fn to_cache_video(video: Video) -> PathBuf {
-    if let VideoStatus::Cached { cache_path, .. } = video.status {
-        cache_path
-    } else {
-        unreachable!("ALl of these videos should be cached.")
-    }
-}
-
-#[derive(Debug)]
-pub struct PlaylistIterator {
-    paths: VecDeque<PathBuf>,
-}
-
-impl Iterator for PlaylistIterator {
-    type Item = <Playlist as IntoIterator>::Item;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        self.paths.pop_front()
-    }
-}
-
-impl DoubleEndedIterator for PlaylistIterator {
-    fn next_back(&mut self) -> Option<Self::Item> {
-        self.paths.pop_back()
-    }
-}
-
-impl IntoIterator for Playlist {
-    type Item = PathBuf;
-
-    type IntoIter = PlaylistIterator;
-
-    fn into_iter(self) -> Self::IntoIter {
-        let paths = self.videos.into_iter().map(to_cache_video).collect();
-        Self::IntoIter { paths }
-    }
-}
-
-#[derive(Debug)]
-pub struct PlaylistIteratorBorrowed<'a> {
-    paths: Vec<&'a Path>,
-    index: usize,
-}
-
-impl<'a> Iterator for PlaylistIteratorBorrowed<'a> {
-    type Item = <&'a Playlist as IntoIterator>::Item;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        let output = self.paths.get(self.index);
-        self.index += 1;
-        output.map(|v| &**v)
-    }
-}
-
-impl<'a> Playlist {
-    #[must_use]
-    pub fn iter(&'a self) -> PlaylistIteratorBorrowed<'a> {
-        <&Self as IntoIterator>::into_iter(self)
-    }
-}
-
-impl<'a> IntoIterator for &'a Playlist {
-    type Item = &'a Path;
-
-    type IntoIter = PlaylistIteratorBorrowed<'a>;
-
-    fn into_iter(self) -> Self::IntoIter {
-        let paths = self
-            .videos
-            .iter()
-            .map(|vid| {
-                if let VideoStatus::Cached { cache_path, .. } = &vid.status {
-                    cache_path.as_path()
-                } else {
-                    unreachable!("ALl of these videos should be cached.")
-                }
-            })
-            .collect();
-        Self::IntoIter { paths, index: 0 }
-    }
-}
diff --git a/crates/yt/src/storage/video_database/get/playlist/mod.rs b/crates/yt/src/storage/video_database/get/playlist/mod.rs
deleted file mode 100644
index f6aadbf..0000000
--- a/crates/yt/src/storage/video_database/get/playlist/mod.rs
+++ /dev/null
@@ -1,167 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-//! This file contains the getters for the internal playlist
-
-use std::{ops::Add, path::PathBuf};
-
-use crate::{
-    app::App,
-    storage::video_database::{Video, VideoStatusMarker, extractor_hash::ExtractorHash},
-    video_from_record,
-};
-
-use anyhow::Result;
-use sqlx::query;
-
-pub mod iterator;
-
-/// Zero-based index into the internal playlist.
-#[derive(Debug, Clone, Copy)]
-pub struct PlaylistIndex(usize);
-
-impl From<PlaylistIndex> for usize {
-    fn from(value: PlaylistIndex) -> Self {
-        value.0
-    }
-}
-
-impl From<usize> for PlaylistIndex {
-    fn from(value: usize) -> Self {
-        Self(value)
-    }
-}
-
-impl Add<usize> for PlaylistIndex {
-    type Output = Self;
-
-    fn add(self, rhs: usize) -> Self::Output {
-        Self(self.0 + rhs)
-    }
-}
-
-impl Add for PlaylistIndex {
-    type Output = Self;
-
-    fn add(self, rhs: Self) -> Self::Output {
-        Self(self.0 + rhs.0)
-    }
-}
-
-/// A representation of the internal Playlist
-#[derive(Debug)]
-pub struct Playlist {
-    videos: Vec<Video>,
-}
-
-impl Playlist {
-    /// Return the videos of this playlist.
-    #[must_use]
-    pub fn as_videos(&self) -> &[Video] {
-        &self.videos
-    }
-
-    /// Turn this playlist to it's videos
-    #[must_use]
-    pub fn to_videos(self) -> Vec<Video> {
-        self.videos
-    }
-
-    /// Find the index of the video specified by the `video_hash`.
-    ///
-    /// # Panics
-    /// Only if internal assertions fail.
-    #[must_use]
-    pub fn find_index(&self, video_hash: &ExtractorHash) -> Option<PlaylistIndex> {
-        if let Some((index, value)) = self
-            .videos
-            .iter()
-            .enumerate()
-            .find(|(_, other)| other.extractor_hash == *video_hash)
-        {
-            assert_eq!(value.extractor_hash, *video_hash);
-            Some(PlaylistIndex(index))
-        } else {
-            None
-        }
-    }
-
-    /// Select a video based on it's index
-    #[must_use]
-    pub fn get(&self, index: PlaylistIndex) -> Option<&Video> {
-        self.videos.get(index.0)
-    }
-
-    /// Returns the number of videos in the playlist
-    #[must_use]
-    pub fn len(&self) -> usize {
-        self.videos.len()
-    }
-    /// Is the playlist empty?
-    #[must_use]
-    pub fn is_empty(&self) -> bool {
-        self.videos.is_empty()
-    }
-}
-
-/// Return the current playlist index.
-///
-/// This effectively looks for the currently focused video and returns it's index.
-///
-/// # Panics
-/// Only if internal assertions fail.
-pub async fn current_playlist_index(app: &App) -> Result<Option<PlaylistIndex>> {
-    if let Some(focused) = currently_focused_video(app).await? {
-        let playlist = playlist(app).await?;
-        let index = playlist
-            .find_index(&focused.extractor_hash)
-            .expect("All focused videos must also be in the playlist");
-        Ok(Some(index))
-    } else {
-        Ok(None)
-    }
-}
-
-/// Return the video in the playlist at the position `index`.
-pub async fn playlist_entry(app: &App, index: PlaylistIndex) -> Result<Option<Video>> {
-    let playlist = playlist(app).await?;
-
-    if let Some(vid) = playlist.get(index) {
-        Ok(Some(vid.to_owned()))
-    } else {
-        Ok(None)
-    }
-}
-
-pub async fn playlist(app: &App) -> Result<Playlist> {
-    let videos = super::videos(app, &[VideoStatusMarker::Cached]).await?;
-
-    Ok(Playlist { videos })
-}
-
-/// This returns the video with the `is_focused` flag set.
-/// # Panics
-/// Only if assertions fail.
-pub async fn currently_focused_video(app: &App) -> Result<Option<Video>> {
-    let cached_status = VideoStatusMarker::Cached.as_db_integer();
-    let record = query!(
-        "SELECT * FROM videos WHERE is_focused = 1 AND status = ?",
-        cached_status
-    )
-    .fetch_one(&app.database)
-    .await;
-
-    if let Err(sqlx::Error::RowNotFound) = record {
-        Ok(None)
-    } else {
-        let base = record?;
-        Ok(Some(video_from_record! {base}))
-    }
-}
diff --git a/crates/yt/src/storage/video_database/set/mod.rs b/crates/yt/src/storage/video_database/set/mod.rs
deleted file mode 100644
index 1b19011..0000000
--- a/crates/yt/src/storage/video_database/set/mod.rs
+++ /dev/null
@@ -1,327 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-//! These functions change the database. They are added on a demand basis.
-
-use std::path::{Path, PathBuf};
-
-use anyhow::{Context, Result};
-use chrono::Utc;
-use log::{debug, info};
-use sqlx::query;
-use tokio::fs;
-
-use crate::{app::App, storage::video_database::extractor_hash::ExtractorHash, video_from_record};
-
-use super::{Priority, Video, VideoOptions, VideoStatus};
-
-mod playlist;
-pub use playlist::*;
-
-const fn is_focused_to_value(is_focused: bool) -> Option<i8> {
-    if is_focused { Some(1) } else { None }
-}
-
-/// Set a new status for a video.
-/// This will only update the status time stamp/priority when the status or the priority has changed .
-pub async fn video_status(
-    app: &App,
-    video_hash: &ExtractorHash,
-    new_status: VideoStatus,
-    new_priority: Option<Priority>,
-) -> Result<()> {
-    let video_hash = video_hash.hash().to_string();
-
-    let old = {
-        let base = query!(
-            r#"
-    SELECT *
-    FROM videos
-    WHERE extractor_hash = ?
-    "#,
-            video_hash
-        )
-        .fetch_one(&app.database)
-        .await?;
-
-        video_from_record! {base}
-    };
-
-    let old_marker = old.status.as_marker();
-    let (cache_path, is_focused) = {
-        fn cache_path_to_string(path: &Path) -> Result<String> {
-            Ok(path
-                .to_str()
-                .with_context(|| {
-                    format!(
-                        "Failed to parse cache path ('{}') as utf8 string",
-                        path.display()
-                    )
-                })?
-                .to_owned())
-        }
-
-        if let VideoStatus::Cached {
-            cache_path,
-            is_focused,
-        } = &new_status
-        {
-            (
-                Some(cache_path_to_string(cache_path)?),
-                is_focused_to_value(*is_focused),
-            )
-        } else {
-            (None, None)
-        }
-    };
-
-    let new_status = new_status.as_marker();
-
-    if let Some(new_priority) = new_priority {
-        if old_marker == new_status && old.priority == new_priority {
-            return Ok(());
-        }
-
-        let now = Utc::now().timestamp();
-
-        debug!("Running status change: {old_marker:#?} -> {new_status:#?}...",);
-
-        let new_status = new_status.as_db_integer();
-        let new_priority = new_priority.as_db_integer();
-        query!(
-            r#"
-        UPDATE videos
-        SET status = ?, last_status_change = ?, priority = ?, cache_path = ?, is_focused = ?
-        WHERE extractor_hash = ?;
-        "#,
-            new_status,
-            now,
-            new_priority,
-            cache_path,
-            is_focused,
-            video_hash
-        )
-        .execute(&app.database)
-        .await?;
-    } else {
-        if old_marker == new_status {
-            return Ok(());
-        }
-
-        let now = Utc::now().timestamp();
-
-        debug!("Running status change: {old_marker:#?} -> {new_status:#?}...",);
-
-        let new_status = new_status.as_db_integer();
-        query!(
-            r#"
-        UPDATE videos
-        SET status = ?, last_status_change = ?, cache_path = ?, is_focused = ?
-        WHERE extractor_hash = ?;
-        "#,
-            new_status,
-            now,
-            cache_path,
-            is_focused,
-            video_hash
-        )
-        .execute(&app.database)
-        .await?;
-    }
-
-    debug!("Finished status change.");
-    Ok(())
-}
-
-/// Mark a video as watched.
-/// This will both set the status to `Watched` and the `cache_path` to Null.
-///
-/// # Panics
-/// Only if assertions fail.
-pub async fn video_watched(app: &App, video: &ExtractorHash) -> Result<()> {
-    let old = {
-        let video_hash = video.hash().to_string();
-
-        let base = query!(
-            r#"
-    SELECT *
-    FROM videos
-    WHERE extractor_hash = ?
-    "#,
-            video_hash
-        )
-        .fetch_one(&app.database)
-        .await?;
-
-        video_from_record! {base}
-    };
-
-    info!("Will set video watched: '{}'", old.title);
-
-    if let VideoStatus::Cached { cache_path, .. } = &old.status {
-        if let Ok(true) = cache_path.try_exists() {
-            fs::remove_file(cache_path).await?;
-        }
-    } else {
-        unreachable!("The video must be marked as Cached before it can be marked Watched");
-    }
-
-    video_status(app, video, VideoStatus::Watched, None).await?;
-
-    Ok(())
-}
-
-pub(crate) async fn video_watch_progress(
-    app: &App,
-    extractor_hash: &ExtractorHash,
-    watch_progress: u32,
-) -> std::result::Result<(), anyhow::Error> {
-    let video_extractor_hash = extractor_hash.hash().to_string();
-
-    query!(
-        r#"
-            UPDATE videos
-            SET watch_progress = ?
-            WHERE extractor_hash = ?;
-        "#,
-        watch_progress,
-        video_extractor_hash,
-    )
-    .execute(&app.database)
-    .await?;
-
-    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(())
-}
-
-/// # Panics
-/// Only if internal expectations fail.
-pub async fn add_video(app: &App, video: Video) -> Result<()> {
-    let parent_subscription_name = video.parent_subscription_name;
-
-    let thumbnail_url = video.thumbnail_url.map(|val| val.to_string());
-
-    let url = video.url.to_string();
-    let extractor_hash = video.extractor_hash.hash().to_string();
-
-    let default_subtitle_langs = &app.config.select.subtitle_langs;
-    let default_mpv_playback_speed = app.config.select.playback_speed;
-
-    let status = video.status.as_marker().as_db_integer();
-    let (cache_path, is_focused) = if let VideoStatus::Cached {
-        cache_path,
-        is_focused,
-    } = video.status
-    {
-        (
-            Some(
-                cache_path
-                    .to_str()
-                    .with_context(|| {
-                        format!(
-                            "Failed to prase cache path '{}' as utf-8 string",
-                            cache_path.display()
-                        )
-                    })?
-                    .to_string(),
-            ),
-            is_focused_to_value(is_focused),
-        )
-    } else {
-        (None, None)
-    };
-
-    let duration: Option<f64> = video.duration.as_secs_f64();
-    let last_status_change: i64 = video.last_status_change.as_secs();
-    let publish_date: Option<i64> = video.publish_date.map(|pd| pd.as_secs());
-    let watch_progress: i64 =
-        i64::try_from(video.watch_progress.as_secs()).expect("This should never exceed a u32");
-
-    let mut tx = app.database.begin().await?;
-    query!(
-        r#"
-        INSERT INTO videos (
-            description,
-            duration,
-            extractor_hash,
-            is_focused,
-            last_status_change,
-            parent_subscription_name,
-            publish_date,
-            status,
-            thumbnail_url,
-            title,
-            url,
-            watch_progress,
-            cache_path
-            )
-        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
-    "#,
-        video.description,
-        duration,
-        extractor_hash,
-        is_focused,
-        last_status_change,
-        parent_subscription_name,
-        publish_date,
-        status,
-        thumbnail_url,
-        video.title,
-        url,
-        watch_progress,
-        cache_path,
-    )
-    .execute(&mut *tx)
-    .await?;
-
-    query!(
-        r#"
-        INSERT INTO video_options (
-            extractor_hash,
-            subtitle_langs,
-            playback_speed)
-        VALUES (?, ?, ?);
-    "#,
-        extractor_hash,
-        default_subtitle_langs,
-        default_mpv_playback_speed
-    )
-    .execute(&mut *tx)
-    .await?;
-
-    tx.commit().await?;
-
-    Ok(())
-}
diff --git a/crates/yt/src/storage/video_database/set/playlist.rs b/crates/yt/src/storage/video_database/set/playlist.rs
deleted file mode 100644
index 547df21..0000000
--- a/crates/yt/src/storage/video_database/set/playlist.rs
+++ /dev/null
@@ -1,101 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use anyhow::Result;
-use log::debug;
-use sqlx::query;
-
-use crate::{
-    app::App,
-    storage::video_database::{extractor_hash::ExtractorHash, get},
-};
-
-/// Set a video to be focused.
-/// This optionally takes another video hash, which marks the old focused video.
-/// This will then be disabled.
-///
-/// # Panics
-/// Only if internal assertions fail.
-pub async fn focused(
-    app: &App,
-    new_video_hash: &ExtractorHash,
-    old_video_hash: Option<&ExtractorHash>,
-) -> Result<()> {
-    unfocused(app, old_video_hash).await?;
-
-    debug!("Focusing video: '{new_video_hash}'");
-    let new_hash = new_video_hash.hash().to_string();
-    query!(
-        r#"
-            UPDATE videos
-            SET is_focused = 1
-            WHERE extractor_hash = ?;
-        "#,
-        new_hash,
-    )
-    .execute(&app.database)
-    .await?;
-
-    assert_eq!(
-        *new_video_hash,
-        get::currently_focused_video(app)
-            .await?
-            .expect("This is some at this point")
-            .extractor_hash
-    );
-    Ok(())
-}
-
-/// Set a video to be no longer focused.
-/// This will use the supplied `video_hash` if it is [`Some`], otherwise it will simply un-focus
-/// the currently focused video.
-///
-/// # Panics
-/// Only if internal assertions fail.
-pub async fn unfocused(app: &App, video_hash: Option<&ExtractorHash>) -> Result<()> {
-    let hash = if let Some(hash) = video_hash {
-        hash.hash().to_string()
-    } else {
-        let output = query!(
-            r#"
-                SELECT extractor_hash
-                FROM videos
-                WHERE is_focused = 1;
-            "#,
-        )
-        .fetch_optional(&app.database)
-        .await?;
-
-        if let Some(output) = output {
-            output.extractor_hash
-        } else {
-            // There is no unfocused video right now.
-            return Ok(());
-        }
-    };
-    debug!("Unfocusing video: '{hash}'");
-
-    query!(
-        r#"
-            UPDATE videos
-            SET is_focused = NULL
-            WHERE extractor_hash = ?;
-        "#,
-        hash
-    )
-    .execute(&app.database)
-    .await?;
-
-    assert!(
-        get::currently_focused_video(app).await?.is_none(),
-        "We assumed that the video we just removed was actually a focused one."
-    );
-    Ok(())
-}