about summary refs log tree commit diff stats
path: root/src/storage
diff options
context:
space:
mode:
Diffstat (limited to 'src/storage')
-rw-r--r--src/storage/mod.rs12
-rw-r--r--src/storage/subscriptions.rs140
-rw-r--r--src/storage/video_database/downloader.rs153
-rw-r--r--src/storage/video_database/extractor_hash.rs159
-rw-r--r--src/storage/video_database/getters.rs345
-rw-r--r--src/storage/video_database/mod.rs179
-rw-r--r--src/storage/video_database/schema.sql57
-rw-r--r--src/storage/video_database/setters.rs270
8 files changed, 0 insertions, 1315 deletions
diff --git a/src/storage/mod.rs b/src/storage/mod.rs
deleted file mode 100644
index 6a12d8b..0000000
--- a/src/storage/mod.rs
+++ /dev/null
@@ -1,12 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-pub mod subscriptions;
-pub mod video_database;
diff --git a/src/storage/subscriptions.rs b/src/storage/subscriptions.rs
deleted file mode 100644
index 22edd08..0000000
--- a/src/storage/subscriptions.rs
+++ /dev/null
@@ -1,140 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-//! Handle subscriptions
-
-use std::collections::HashMap;
-
-use anyhow::Result;
-use log::debug;
-use serde_json::{json, Value};
-use sqlx::query;
-use url::Url;
-use yt_dlp::wrapper::info_json::InfoType;
-
-use crate::app::App;
-
-#[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 {
-    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_opts = match json!( {
-        "playliststart": 1,
-        "playlistend": 10,
-        "noplaylist": false,
-        "extract_flat": "in_playlist",
-    }) {
-        Value::Object(map) => map,
-        _ => unreachable!("This is hardcoded"),
-    };
-
-    let info = yt_dlp::extract_info(&yt_opts, url, false, false).await?;
-
-    debug!("{:#?}", info);
-
-    Ok(info._type == Some(InfoType::Playlist))
-}
-
-#[derive(Default)]
-pub struct Subscriptions(pub(crate) HashMap<String, Subscription>);
-
-pub async fn remove_all_subscriptions(app: &App) -> Result<()> {
-    query!(
-        "
-        DELETE FROM subscriptions;
-    ",
-    )
-    .execute(&app.database)
-    .await?;
-
-    Ok(())
-}
-
-/// Get a list of subscriptions
-pub async fn get_subscriptions(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).expect("This should be valid"),
-                ),
-            )
-        })
-        .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(())
-}
-
-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/src/storage/video_database/downloader.rs b/src/storage/video_database/downloader.rs
deleted file mode 100644
index ccd4ca9..0000000
--- a/src/storage/video_database/downloader.rs
+++ /dev/null
@@ -1,153 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::path::{Path, PathBuf};
-
-use anyhow::Result;
-use log::debug;
-use sqlx::query;
-use url::Url;
-
-use crate::{app::App, storage::video_database::VideoStatus};
-
-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.
-pub async fn get_next_uncached_video(app: &App) -> Result<Option<Video>> {
-    let status = VideoStatus::Watch.as_db_integer();
-
-    // NOTE: The ORDER BY statement should be the same as the one in [`getters::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?;
-
-        let thumbnail_url = base
-            .thumbnail_url
-            .as_ref()
-            .map(|url| Url::parse(url).expect("Parsing this as url should always work"));
-
-        let status_change = if base.status_change == 1 {
-            true
-        } else {
-            assert_eq!(base.status_change, 0, "Can only be 1 or 0");
-            false
-        };
-
-        let video = Video {
-            cache_path: base.cache_path.as_ref().map(PathBuf::from),
-            description: base.description.clone(),
-            duration: base.duration,
-            extractor_hash: ExtractorHash::from_hash(
-                base.extractor_hash
-                    .parse()
-                    .expect("The hash in the db should be valid"),
-            ),
-            last_status_change: base.last_status_change,
-            parent_subscription_name: base.parent_subscription_name.clone(),
-            priority: base.priority,
-            publish_date: base.publish_date,
-            status: VideoStatus::from_db_integer(base.status),
-            status_change,
-            thumbnail_url,
-            title: base.title.clone(),
-            url: Url::parse(&base.url).expect("Parsing this as url should always work"),
-        };
-
-        Ok(Some(video))
-    }
-}
-
-/// 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 = VideoStatus::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_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(count.count as u32)
-}
diff --git a/src/storage/video_database/extractor_hash.rs b/src/storage/video_database/extractor_hash.rs
deleted file mode 100644
index c956919..0000000
--- a/src/storage/video_database/extractor_hash.rs
+++ /dev/null
@@ -1,159 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::{collections::HashMap, fmt::Display, str::FromStr};
-
-use anyhow::{bail, Context, Result};
-use blake3::Hash;
-use log::debug;
-use tokio::sync::OnceCell;
-
-use crate::{app::App, storage::video_database::getters::get_all_hashes};
-
-static EXTRACTOR_HASH_LENGTH: OnceCell<usize> = OnceCell::const_new();
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct ExtractorHash {
-    hash: Hash,
-}
-
-impl Display for ExtractorHash {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        self.hash.fmt(f)
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct ShortHash(String);
-
-impl Display for ShortHash {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        self.0.fmt(f)
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct LazyExtractorHash {
-    value: ShortHash,
-}
-
-impl FromStr for LazyExtractorHash {
-    type Err = anyhow::Error;
-
-    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
-        // perform some cheap validation
-        if s.len() > 64 {
-            bail!("A hash can only contain 64 bytes!");
-        }
-
-        Ok(Self {
-            value: ShortHash(s.to_owned()),
-        })
-    }
-}
-
-impl LazyExtractorHash {
-    /// Turn the [`LazyExtractorHash`] into the [`ExtractorHash`]
-    pub async fn realize(self, app: &App) -> Result<ExtractorHash> {
-        ExtractorHash::from_short_hash(app, &self.value).await
-    }
-}
-
-impl ExtractorHash {
-    pub fn from_hash(hash: Hash) -> Self {
-        Self { hash }
-    }
-    pub async fn from_short_hash(app: &App, s: &ShortHash) -> Result<Self> {
-        Ok(Self {
-            hash: Self::short_hash_to_full_hash(app, s).await?,
-        })
-    }
-
-    pub fn hash(&self) -> &Hash {
-        &self.hash
-    }
-
-    pub 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 {
-            let needed_chars = self
-                .get_needed_char_len(app)
-                .await
-                .context("Failed to calculate needed char length")?;
-            EXTRACTOR_HASH_LENGTH
-                .set(needed_chars)
-                .expect("This should work at this stage");
-
-            needed_chars
-        };
-
-        Ok(ShortHash(
-            self.hash()
-                .to_hex()
-                .chars()
-                .take(needed_chars)
-                .collect::<String>(),
-        ))
-    }
-
-    async fn short_hash_to_full_hash(app: &App, s: &ShortHash) -> Result<Hash> {
-        let all_hashes = get_all_hashes(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 {
-                return Ok(hash);
-            }
-        }
-
-        bail!("Your shortend hash, does not match a real hash (this is probably a bug)!");
-    }
-
-    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)
-            .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>>())
-            .collect::<Vec<Vec<_>>>();
-
-        // This value should be updated later, if not rust will panic in the assertion.
-        let mut needed_chars: usize = 1000;
-        'outer: for i in 1..64 {
-            let i_chars: Vec<String> = all_char_vec_hashes
-                .iter()
-                .map(|vec| vec.iter().take(i).collect::<String>())
-                .collect();
-
-            let mut uniqnes_hashmap: HashMap<String, ()> = HashMap::new();
-            for ch in i_chars {
-                if let Some(()) = uniqnes_hashmap.insert(ch, ()) {
-                    // The key was already in the hash map, thus we have a duplicated char and need
-                    // at least one char more
-                    continue 'outer;
-                }
-            }
-
-            needed_chars = i;
-            break 'outer;
-        }
-
-        assert!(needed_chars <= 64, "Hashes are only 64 bytes long");
-
-        Ok(needed_chars)
-    }
-}
diff --git a/src/storage/video_database/getters.rs b/src/storage/video_database/getters.rs
deleted file mode 100644
index 29dd014..0000000
--- a/src/storage/video_database/getters.rs
+++ /dev/null
@@ -1,345 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-//! These functions interact with the storage db in a read-only way. They are added on-demaned (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::{bail, Context, Result};
-use blake3::Hash;
-use log::debug;
-use sqlx::{query, QueryBuilder, Row, Sqlite};
-use url::Url;
-use yt_dlp::wrapper::info_json::InfoJson;
-
-use crate::{
-    app::App,
-    storage::{
-        subscriptions::Subscription,
-        video_database::{extractor_hash::ExtractorHash, Video},
-    },
-};
-
-use super::{MpvOptions, VideoOptions, VideoStatus, YtDlpOptions};
-
-macro_rules! video_from_record {
-    ($record:expr) => {
-        let thumbnail_url = if let Some(url) = &$record.thumbnail_url {
-            Some(Url::parse(&url).expect("Parsing this as url should always work"))
-        } else {
-            None
-        };
-
-        Ok(Video {
-            cache_path: $record.cache_path.as_ref().map(|val| PathBuf::from(val)),
-            description: $record.description.clone(),
-            duration: $record.duration,
-            extractor_hash: ExtractorHash::from_hash(
-                $record
-                    .extractor_hash
-                    .parse()
-                    .expect("The db hash should be a valid blake3 hash"),
-            ),
-            last_status_change: $record.last_status_change,
-            parent_subscription_name: $record.parent_subscription_name.clone(),
-            publish_date: $record.publish_date,
-            status: VideoStatus::from_db_integer($record.status),
-            thumbnail_url,
-            title: $record.title.clone(),
-            url: Url::parse(&$record.url).expect("Parsing this as url should always work"),
-            priority: $record.priority,
-            status_change: if $record.status_change == 1 {
-                true
-            } else {
-                assert_eq!($record.status_change, 0);
-                false
-            },
-        })
-    };
-}
-
-/// Get the lines to display at the selection file
-/// [`changing` = true]: Means that we include *only* videos, that have the `status_changing` flag set
-/// [`changing` = None]: Means that we include *both* videos, that have the `status_changing` flag set and not set
-pub async fn get_videos(
-    app: &App,
-    allowed_states: &[VideoStatus],
-    changing: Option<bool>,
-) -> Result<Vec<Video>> {
-    let mut qb: QueryBuilder<Sqlite> = QueryBuilder::new(
-        "\
-    SELECT *
-    FROM videos
-    WHERE status IN ",
-    );
-
-    qb.push("(");
-    allowed_states
-        .iter()
-        .enumerate()
-        .for_each(|(index, state)| {
-            qb.push("'");
-            qb.push(state.as_db_integer());
-            qb.push("'");
-
-            if index != allowed_states.len() - 1 {
-                qb.push(",");
-            }
-        });
-    qb.push(")");
-
-    if let Some(val) = changing {
-        if val {
-            qb.push(" AND status_change = 1");
-        } else {
-            qb.push(" AND status_change = 0");
-        }
-    }
-
-    qb.push("\n    ORDER BY priority DESC, publish_date DESC;");
-
-    debug!("Will run: \"{}\"", qb.sql());
-
-    let videos = qb.build().fetch_all(&app.database).await.with_context(|| {
-        format!(
-            "Failed to query videos with states: '{}'",
-            allowed_states.iter().fold(String::new(), |mut acc, state| {
-                acc.push(' ');
-                acc.push_str(state.as_str());
-                acc
-            }),
-        )
-    })?;
-
-    let real_videos: Vec<Video> = videos
-        .iter()
-        .map(|base| -> Result<Video> {
-            Ok(Video {
-                cache_path: base
-                    .get::<Option<String>, &str>("cache_path")
-                    .as_ref()
-                    .map(PathBuf::from),
-                description: base.get::<Option<String>, &str>("description").clone(),
-                duration: base.get("duration"),
-                extractor_hash: ExtractorHash::from_hash(
-                    base.get::<String, &str>("extractor_hash")
-                        .parse()
-                        .expect("The db hash should be a valid blake3 hash"),
-                ),
-                last_status_change: base.get("last_status_change"),
-                parent_subscription_name: base
-                    .get::<Option<String>, &str>("parent_subscription_name")
-                    .clone(),
-                publish_date: base.get("publish_date"),
-                status: VideoStatus::from_db_integer(base.get("status")),
-                thumbnail_url: base
-                    .get::<Option<String>, &str>("thumbnail_url")
-                    .as_ref()
-                    .map(|url| Url::parse(url).expect("Parsing this as url should always work")),
-                title: base.get::<String, &str>("title").to_owned(),
-                url: Url::parse(base.get("url")).expect("Parsing this as url should always work"),
-                priority: base.get("priority"),
-                status_change: {
-                    let val = base.get::<i64, &str>("status_change");
-                    if val == 1 {
-                        true
-                    } else {
-                        assert_eq!(val, 0, "Can only be 1 or 0");
-                        false
-                    }
-                },
-            })
-        })
-        .collect::<Result<Vec<Video>>>()?;
-
-    Ok(real_videos)
-}
-
-pub async fn get_video_info_json(video: &Video) -> Result<Option<InfoJson>> {
-    if let Some(mut path) = video.cache_path.clone() {
-        if !path.set_extension("info.json") {
-            bail!(
-                "Failed to change path extension to 'info.json': {}",
-                path.display()
-            );
-        }
-        let info_json_string = File::open(path)?;
-        let info_json: InfoJson = serde_json::from_reader(&info_json_string)?;
-
-        Ok(Some(info_json))
-    } else {
-        Ok(None)
-    }
-}
-
-pub async fn get_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?;
-
-    video_from_record! {raw_video}
-}
-
-pub async fn get_currently_playing_video(app: &App) -> Result<Option<Video>> {
-    let mut videos: Vec<Video> = get_changing_videos(app, VideoStatus::Cached).await?;
-
-    if videos.is_empty() {
-        Ok(None)
-    } else {
-        assert_eq!(
-            videos.len(),
-            1,
-            "Only one video can change from cached to watched at once!"
-        );
-
-        Ok(Some(videos.remove(0)))
-    }
-}
-
-pub async fn get_changing_videos(app: &App, old_state: VideoStatus) -> Result<Vec<Video>> {
-    let status = old_state.as_db_integer();
-
-    let matching = query!(
-        r#"
-        SELECT *
-        FROM videos
-        WHERE status_change = 1 AND status = ?;
-    "#,
-        status
-    )
-    .fetch_all(&app.database)
-    .await?;
-
-    let real_videos: Vec<Video> = matching
-        .iter()
-        .map(|base| -> Result<Video> {
-            video_from_record! {base}
-        })
-        .collect::<Result<Vec<Video>>>()?;
-
-    Ok(real_videos)
-}
-
-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)
-                .expect("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)
-                .expect("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: {}",
-            hash
-        )
-    })?;
-
-    Ok(YtDlpOptions {
-        subtitle_langs: yt_dlp_options.subtitle_langs,
-    })
-}
-pub async fn get_video_mpv_opts(app: &App, hash: &ExtractorHash) -> Result<MpvOptions> {
-    let ehash = hash.hash().to_string();
-
-    let mpv_options = query!(
-        r#"
-        SELECT playback_speed
-        FROM video_options
-        WHERE extractor_hash = ?;
-    "#,
-        ehash
-    )
-    .fetch_one(&app.database)
-    .await
-    .with_context(|| format!("Failed to fetch the `mpv_video_opts` for video: {}", 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: {}", hash))?;
-
-    let mpv = MpvOptions {
-        playback_speed: opts.playback_speed,
-    };
-    let yt_dlp = YtDlpOptions {
-        subtitle_langs: opts.subtitle_langs,
-    };
-
-    Ok(VideoOptions { mpv, yt_dlp })
-}
diff --git a/src/storage/video_database/mod.rs b/src/storage/video_database/mod.rs
deleted file mode 100644
index 1765f79..0000000
--- a/src/storage/video_database/mod.rs
+++ /dev/null
@@ -1,179 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::{fmt::Write, path::PathBuf};
-
-use url::Url;
-
-use crate::{app::App, storage::video_database::extractor_hash::ExtractorHash};
-
-pub mod downloader;
-pub mod extractor_hash;
-pub mod getters;
-pub mod setters;
-
-#[derive(Debug, Clone)]
-pub struct Video {
-    pub cache_path: Option<PathBuf>,
-    pub description: Option<String>,
-    pub duration: Option<f64>,
-    pub extractor_hash: ExtractorHash,
-    pub last_status_change: i64,
-    /// The associated subscription this video was fetched from (null, when the video was `add`ed)
-    pub parent_subscription_name: Option<String>,
-    pub priority: i64,
-    pub publish_date: Option<i64>,
-    pub status: VideoStatus,
-    /// The video is currently changing its state (for example from being `SELECT` to being `CACHE`)
-    pub status_change: bool,
-    pub thumbnail_url: Option<Url>,
-    pub title: String,
-    pub url: Url,
-}
-
-#[derive(Debug)]
-pub struct VideoOptions {
-    pub yt_dlp: YtDlpOptions,
-    pub mpv: MpvOptions,
-}
-impl VideoOptions {
-    pub(crate) fn new(subtitle_langs: String, playback_speed: f64) -> Self {
-        let yt_dlp = YtDlpOptions { subtitle_langs };
-        let mpv = MpvOptions { playback_speed };
-        Self { yt_dlp, mpv }
-    }
-
-    /// This will write out the options that are different from the defaults.
-    /// Beware, that this does not set the priority.
-    pub fn to_cli_flags(self, app: &App) -> String {
-        let mut f = String::new();
-
-        if self.mpv.playback_speed != app.config.select.playback_speed {
-            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)]
-/// 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>
-///     /    \
-/// <Watch>   <Drop> -> Dropped // yt select
-///     |
-/// Cache                       // yt cache
-///     |
-/// Watched                     // yt watch
-#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
-pub enum VideoStatus {
-    #[default]
-    Pick,
-
-    /// The video has been select to be watched
-    Watch,
-    /// The video has been cached and is ready to be watched
-    Cached,
-    /// The video has been watched
-    Watched,
-
-    /// The video has been select to be dropped
-    Drop,
-    /// The video has been dropped
-    Dropped,
-}
-
-impl VideoStatus {
-    pub const ALL: &'static [Self; 6] = &[
-        Self::Pick,
-        //
-        VideoStatus::Watch,
-        VideoStatus::Cached,
-        VideoStatus::Watched,
-        //
-        VideoStatus::Drop,
-        VideoStatus::Dropped,
-    ];
-
-    pub 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 {
-            VideoStatus::Pick => "pick   ",
-
-            VideoStatus::Watch => "watch  ",
-            VideoStatus::Cached => "watch  ",
-            VideoStatus::Watched => "watched",
-
-            VideoStatus::Drop => "drop   ",
-            VideoStatus::Dropped => "drop   ",
-        }
-    }
-
-    pub 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 {
-            VideoStatus::Pick => 0,
-
-            VideoStatus::Watch => 1,
-            VideoStatus::Cached => 2,
-            VideoStatus::Watched => 3,
-
-            VideoStatus::Drop => 4,
-            VideoStatus::Dropped => 5,
-        }
-    }
-    pub fn from_db_integer(num: i64) -> Self {
-        match num {
-            0 => Self::Pick,
-
-            1 => Self::Watch,
-            2 => Self::Cached,
-            3 => Self::Watched,
-
-            4 => Self::Drop,
-            5 => Self::Dropped,
-            other => unreachable!(
-                "The database returned a enum discriminator, unknown to us: '{}'",
-                other
-            ),
-        }
-    }
-
-    pub fn as_str(&self) -> &'static str {
-        match self {
-            VideoStatus::Pick => "Pick",
-
-            VideoStatus::Watch => "Watch",
-            VideoStatus::Cached => "Cache",
-            VideoStatus::Watched => "Watched",
-
-            VideoStatus::Drop => "Drop",
-            VideoStatus::Dropped => "Dropped",
-        }
-    }
-}
diff --git a/src/storage/video_database/schema.sql b/src/storage/video_database/schema.sql
deleted file mode 100644
index 3afd091..0000000
--- a/src/storage/video_database/schema.sql
+++ /dev/null
@@ -1,57 +0,0 @@
--- yt - A fully featured command line YouTube client
---
--- Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
--- SPDX-License-Identifier: GPL-3.0-or-later
---
--- This file is part of Yt.
---
--- You should have received a copy of the License along with this program.
--- If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
--- All tables should be declared STRICT, as I actually like to have types checking (and a
--- db that doesn't lie to me).
-
--- Keep this table in sync with the `Video` structure
-CREATE TABLE IF NOT EXISTS videos (
-    cache_path                  TEXT UNIQUE                    CHECK (CASE WHEN cache_path IS NOT NULL THEN
-                                                                            status == 2
-                                                                      ELSE
-                                                                            1
-                                                                      END),
-    description                 TEXT,
-    duration                    REAL,
-    extractor_hash              TEXT UNIQUE NOT NULL PRIMARY KEY,
-    last_status_change          INTEGER     NOT NULL,
-    parent_subscription_name    TEXT,
-    priority                    INTEGER     NOT NULL DEFAULT 0,
-    publish_date                INTEGER,
-    status                      INTEGER     NOT NULL DEFAULT 0 CHECK (status IN (0, 1, 2, 3, 4, 5) AND
-                                                                      CASE WHEN status == 2 THEN
-                                                                           cache_path IS NOT NULL
-                                                                      ELSE
-                                                                           1
-                                                                      END AND
-                                                                      CASE WHEN status != 2 THEN
-                                                                           cache_path IS NULL
-                                                                      ELSE
-                                                                           1
-                                                                      END),
-    status_change               INTEGER     NOT NULL DEFAULT 0 CHECK (status_change IN (0, 1)),
-    thumbnail_url               TEXT,
-    title                       TEXT        NOT NULL,
-    url                         TEXT UNIQUE NOT NULL
-) STRICT;
-
--- Store additional metadata for the videos marked to be watched
-CREATE TABLE IF NOT EXISTS video_options (
-    extractor_hash              TEXT UNIQUE NOT NULL PRIMARY KEY,
-    subtitle_langs              TEXT        NOT NULL,
-    playback_speed              REAL        NOT NULL,
-    FOREIGN KEY(extractor_hash) REFERENCES videos (extractor_hash)
-) STRICT;
-
--- Store subscriptions
-CREATE TABLE IF NOT EXISTS subscriptions (
-    name              TEXT UNIQUE NOT NULL PRIMARY KEY,
-    url               TEXT        NOT NULL
-) STRICT;
diff --git a/src/storage/video_database/setters.rs b/src/storage/video_database/setters.rs
deleted file mode 100644
index c160138..0000000
--- a/src/storage/video_database/setters.rs
+++ /dev/null
@@ -1,270 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-//! These functions change the database. They are added on a demand basis.
-
-use anyhow::Result;
-use chrono::Utc;
-use log::{debug, info};
-use sqlx::query;
-use tokio::fs;
-
-use crate::{app::App, storage::video_database::extractor_hash::ExtractorHash};
-
-use super::{Video, VideoOptions, VideoStatus};
-
-/// Set a new status for a video.
-/// This will only update the status time stamp/priority when the status or the priority has changed .
-pub async fn set_video_status(
-    app: &App,
-    video_hash: &ExtractorHash,
-    new_status: VideoStatus,
-    new_priority: Option<i64>,
-) -> Result<()> {
-    let video_hash = video_hash.hash().to_string();
-
-    let old = query!(
-        r#"
-    SELECT status, priority, cache_path
-    FROM videos
-    WHERE extractor_hash = ?
-    "#,
-        video_hash
-    )
-    .fetch_one(&app.database)
-    .await?;
-
-    let cache_path = if (VideoStatus::from_db_integer(old.status) == VideoStatus::Cached)
-        && (new_status != VideoStatus::Cached)
-    {
-        None
-    } else {
-        old.cache_path.as_deref()
-    };
-
-    let new_status = new_status.as_db_integer();
-
-    if let Some(new_priority) = new_priority {
-        if old.status == new_status && old.priority == new_priority {
-            return Ok(());
-        }
-
-        let now = Utc::now().timestamp();
-
-        debug!(
-            "Running status change: {:#?} -> {:#?}...",
-            VideoStatus::from_db_integer(old.status),
-            VideoStatus::from_db_integer(new_status),
-        );
-
-        query!(
-            r#"
-        UPDATE videos
-        SET status = ?, last_status_change = ?, priority = ?, cache_path = ?
-        WHERE extractor_hash = ?;
-        "#,
-            new_status,
-            now,
-            new_priority,
-            cache_path,
-            video_hash
-        )
-        .execute(&app.database)
-        .await?;
-    } else {
-        if old.status == new_status {
-            return Ok(());
-        }
-
-        let now = Utc::now().timestamp();
-
-        debug!(
-            "Running status change: {:#?} -> {:#?}...",
-            VideoStatus::from_db_integer(old.status),
-            VideoStatus::from_db_integer(new_status),
-        );
-
-        query!(
-            r#"
-        UPDATE videos
-        SET status = ?, last_status_change = ?, cache_path = ?
-        WHERE extractor_hash = ?;
-        "#,
-            new_status,
-            now,
-            cache_path,
-            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.
-pub async fn set_video_watched(app: &App, video: &Video) -> Result<()> {
-    let video_hash = video.extractor_hash.hash().to_string();
-    let new_status = VideoStatus::Watched.as_db_integer();
-
-    info!("Will set video watched: '{}'", video.title);
-
-    let old = query!(
-        r#"
-    SELECT status, priority
-    FROM videos
-    WHERE extractor_hash = ?
-    "#,
-        video_hash
-    )
-    .fetch_one(&app.database)
-    .await?;
-
-    assert_ne!(
-        old.status, new_status,
-        "The video should not be marked as watched already."
-    );
-    assert_eq!(
-        old.status,
-        VideoStatus::Cached.as_db_integer(),
-        "The video should have been marked cached"
-    );
-
-    let now = Utc::now().timestamp();
-
-    if let Some(path) = &video.cache_path {
-        if let Ok(true) = path.try_exists() {
-            fs::remove_file(path).await?
-        }
-    }
-
-    query!(
-        r#"
-        UPDATE videos
-        SET status = ?, last_status_change = ?, cache_path = NULL
-        WHERE extractor_hash = ?;
-        "#,
-        new_status,
-        now,
-        video_hash
-    )
-    .execute(&app.database)
-    .await?;
-
-    Ok(())
-}
-
-pub async fn set_state_change(
-    app: &App,
-    video_extractor_hash: &ExtractorHash,
-    changing: bool,
-) -> Result<()> {
-    let state_change = if changing { 1 } else { 0 };
-    let video_extractor_hash = video_extractor_hash.hash().to_string();
-
-    query!(
-        r#"
-            UPDATE videos
-            SET status_change = ?
-            WHERE extractor_hash = ?;
-        "#,
-        state_change,
-        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(())
-}
-
-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 status = video.status.as_db_integer();
-    let status_change = if video.status_change { 1 } else { 0 };
-    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;
-
-    query!(
-        r#"
-        BEGIN;
-        INSERT INTO videos (
-            parent_subscription_name,
-            status,
-            status_change,
-            last_status_change,
-            title,
-            url,
-            description,
-            duration,
-            publish_date,
-            thumbnail_url,
-            extractor_hash)
-        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
-
-        INSERT INTO video_options (
-            extractor_hash,
-            subtitle_langs,
-            playback_speed)
-        VALUES (?, ?, ?);
-        COMMIT;
-    "#,
-        parent_subscription_name,
-        status,
-        status_change,
-        video.last_status_change,
-        video.title,
-        url,
-        video.description,
-        video.duration,
-        video.publish_date,
-        thumbnail_url,
-        extractor_hash,
-        extractor_hash,
-        default_subtitle_langs,
-        default_mpv_playback_speed
-    )
-    .execute(&app.database)
-    .await?;
-
-    Ok(())
-}