about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--yt/src/cache/mod.rs47
-rw-r--r--yt/src/cli.rs4
-rw-r--r--yt/src/comments/mod.rs18
-rw-r--r--yt/src/download/mod.rs2
-rw-r--r--yt/src/main.rs4
-rw-r--r--yt/src/select/cmds/add.rs2
-rw-r--r--yt/src/select/cmds/mod.rs29
-rw-r--r--yt/src/select/mod.rs22
-rw-r--r--yt/src/select/selection_file/duration.rs127
-rw-r--r--yt/src/status/mod.rs82
-rw-r--r--yt/src/storage/migrate/mod.rs3
-rw-r--r--yt/src/storage/video_database/downloader.rs14
-rw-r--r--yt/src/storage/video_database/extractor_hash.rs2
-rw-r--r--yt/src/storage/video_database/getters.rs347
-rw-r--r--yt/src/storage/video_database/mod.rs216
-rw-r--r--yt/src/storage/video_database/setters.rs317
-rw-r--r--yt/src/update/mod.rs19
-rw-r--r--yt/src/videos/display/format_video.rs6
-rw-r--r--yt/src/videos/display/mod.rs61
-rw-r--r--yt/src/videos/mod.rs6
-rw-r--r--yt/src/watch/events/handlers/mod.rs194
-rw-r--r--yt/src/watch/events/mod.rs322
-rw-r--r--yt/src/watch/events/playlist_handler.rs97
-rw-r--r--yt/src/watch/mod.rs103
24 files changed, 474 insertions, 1570 deletions
diff --git a/yt/src/cache/mod.rs b/yt/src/cache/mod.rs
index 6cd240c..f5a0da9 100644
--- a/yt/src/cache/mod.rs
+++ b/yt/src/cache/mod.rs
@@ -15,7 +15,7 @@ use tokio::fs;
 use crate::{
     app::App,
     storage::video_database::{
-        Video, VideoStatus, downloader::set_video_cache_path, getters::get_videos,
+        Video, VideoStatus, VideoStatusMarker, downloader::set_video_cache_path, get,
     },
 };
 
@@ -23,15 +23,17 @@ async fn invalidate_video(app: &App, video: &Video, hard: bool) -> Result<()> {
     info!("Invalidating cache of video: '{}'", video.title);
 
     if hard {
-        if let Some(path) = &video.cache_path {
+        if let VideoStatus::Cached {
+            cache_path: path, ..
+        } = &video.status
+        {
             info!("Removing cached video at: '{}'", path.display());
             if let Err(err) = fs::remove_file(path).await.map_err(|err| err.kind()) {
                 match err {
                     std::io::ErrorKind::NotFound => {
                         // The path is already gone
                         debug!(
-                            "Not actually removing path: '{}'. \
-                             It is already gone.",
+                            "Not actually removing path: '{}'. It is already gone.",
                             path.display()
                         );
                     }
@@ -53,7 +55,7 @@ async fn invalidate_video(app: &App, video: &Video, hard: bool) -> Result<()> {
 }
 
 pub async fn invalidate(app: &App, hard: bool) -> Result<()> {
-    let all_cached_things = get_videos(app, &[VideoStatus::Cached], None).await?;
+    let all_cached_things = get::videos(app, &[VideoStatusMarker::Cached]).await?;
 
     info!("Got videos to invalidate: '{}'", all_cached_things.len());
 
@@ -64,34 +66,39 @@ pub async fn invalidate(app: &App, hard: bool) -> Result<()> {
     Ok(())
 }
 
+/// # Panics
+/// Only if internal assertions fail.
 pub async fn maintain(app: &App, all: bool) -> Result<()> {
     let domain = if all {
-        vec![
-            VideoStatus::Pick,
-            //
-            VideoStatus::Watch,
-            VideoStatus::Cached,
-            VideoStatus::Watched,
-            //
-            VideoStatus::Drop,
-            VideoStatus::Dropped,
-        ]
+        VideoStatusMarker::ALL.as_slice()
     } else {
-        vec![VideoStatus::Watch, VideoStatus::Cached]
+        &[VideoStatusMarker::Watch, VideoStatusMarker::Cached]
     };
 
-    let cached_videos = get_videos(app, domain.as_slice(), None).await?;
+    let cached_videos = get::videos(app, domain).await?;
 
+    let mut found_focused = 0;
     for vid in cached_videos {
-        if let Some(path) = vid.cache_path.as_ref() {
+        if let VideoStatus::Cached {
+            cache_path: path,
+            is_focused,
+        } = &vid.status
+        {
             info!("Checking if path ('{}') exists", path.display());
             if !path.exists() {
                 invalidate_video(app, &vid, false).await?;
             }
-        }
 
-        // TODO(@bpeetz): Check if only one video is set `is_focused`. <2025-02-17>
+            if *is_focused {
+                found_focused += 1;
+            }
+        }
     }
 
+    assert!(
+        found_focused <= 1,
+        "Only one video can be focused at a time"
+    );
+
     Ok(())
 }
diff --git a/yt/src/cli.rs b/yt/src/cli.rs
index fd0dfbe..948138d 100644
--- a/yt/src/cli.rs
+++ b/yt/src/cli.rs
@@ -17,7 +17,7 @@ use clap::{ArgAction, Args, Parser, Subcommand};
 use url::Url;
 
 use crate::{
-    select::selection_file::duration::Duration,
+    select::selection_file::duration::MaybeDuration,
     storage::video_database::extractor_hash::LazyExtractorHash,
 };
 
@@ -233,7 +233,7 @@ pub struct SharedSelectionCommandArgs {
 
     pub publisher: Option<OptionalPublisher>,
 
-    pub duration: Option<Duration>,
+    pub duration: Option<MaybeDuration>,
 
     pub url: Option<Url>,
 }
diff --git a/yt/src/comments/mod.rs b/yt/src/comments/mod.rs
index 97b2c24..1482f15 100644
--- a/yt/src/comments/mod.rs
+++ b/yt/src/comments/mod.rs
@@ -18,10 +18,7 @@ use yt_dlp::wrapper::info_json::{Comment, InfoJson, Parent};
 
 use crate::{
     app::App,
-    storage::video_database::{
-        Video,
-        getters::{get_currently_playing_video, get_video_info_json},
-    },
+    storage::video_database::{Video, get},
     unreachable::Unreachable,
 };
 
@@ -29,20 +26,21 @@ mod comment;
 mod display;
 pub mod output;
 
+pub mod description;
+pub use description::*;
+
 #[allow(clippy::too_many_lines)]
 pub async fn get(app: &App) -> Result<Comments> {
     let currently_playing_video: Video =
-        if let Some(video) = get_currently_playing_video(app).await? {
+        if let Some(video) = get::currently_focused_video(app).await? {
             video
         } else {
             bail!("Could not find a currently playing video!");
         };
 
-    let mut info_json: InfoJson = get_video_info_json(&currently_playing_video)
-        .await?
-        .unreachable(
-            "A currently *playing* must be cached. And thus the info.json should be available",
-        );
+    let mut info_json: InfoJson = get::video_info_json(&currently_playing_video)?.unreachable(
+        "A currently *playing* must be cached. And thus the info.json should be available",
+    );
 
     let base_comments = mem::take(&mut info_json.comments).with_context(|| {
         format!(
diff --git a/yt/src/download/mod.rs b/yt/src/download/mod.rs
index 317f636..168c1b2 100644
--- a/yt/src/download/mod.rs
+++ b/yt/src/download/mod.rs
@@ -17,7 +17,7 @@ use crate::{
         Video, YtDlpOptions,
         downloader::{get_next_uncached_video, set_video_cache_path},
         extractor_hash::ExtractorHash,
-        getters::get_video_yt_dlp_opts,
+        get::get_video_yt_dlp_opts,
         notify::wait_for_cache_reduction,
     },
     unreachable::Unreachable,
diff --git a/yt/src/main.rs b/yt/src/main.rs
index 7c550af..ed24262 100644
--- a/yt/src/main.rs
+++ b/yt/src/main.rs
@@ -23,7 +23,7 @@ use cli::{CacheCommand, CheckCommand, SelectCommand, SubscriptionCommand, Videos
 use config::Config;
 use log::info;
 use select::cmds::handle_select_cmd;
-use storage::video_database::getters::get_video_by_hash;
+use storage::video_database::get::video_by_hash;
 use tokio::{
     fs::File,
     io::{BufReader, stdin},
@@ -140,7 +140,7 @@ async fn main() -> Result<()> {
                     .context("Failed to query videos")?;
             }
             VideosCommand::Info { hash } => {
-                let video = get_video_by_hash(&app, &hash.realize(&app).await?).await?;
+                let video = video_by_hash(&app, &hash.realize(&app).await?).await?;
 
                 print!(
                     "{}",
diff --git a/yt/src/select/cmds/add.rs b/yt/src/select/cmds/add.rs
index 1e14995..154bb0a 100644
--- a/yt/src/select/cmds/add.rs
+++ b/yt/src/select/cmds/add.rs
@@ -2,7 +2,7 @@ use crate::{
     app::App,
     download::download_options::download_opts,
     storage::video_database::{
-        self, extractor_hash::ExtractorHash, getters::get_all_hashes, setters::add_video,
+        self, extractor_hash::ExtractorHash, get::get_all_hashes, set::add_video,
     },
     unreachable::Unreachable,
     update::video_entry_to_video,
diff --git a/yt/src/select/cmds/mod.rs b/yt/src/select/cmds/mod.rs
index 2a8a44f..e7f8594 100644
--- a/yt/src/select/cmds/mod.rs
+++ b/yt/src/select/cmds/mod.rs
@@ -12,9 +12,9 @@ use crate::{
     app::App,
     cli::{SelectCommand, SharedSelectionCommandArgs},
     storage::video_database::{
-        VideoOptions, VideoStatus,
-        getters::get_video_by_hash,
-        setters::{set_video_options, set_video_status},
+        Priority, VideoOptions, VideoStatus,
+        get::video_by_hash,
+        set::{set_video_options, video_status},
     },
 };
 
@@ -41,9 +41,18 @@ pub async fn handle_select_cmd(
         SelectCommand::Watch { shared } => {
             let hash = shared.hash.clone().realize(app).await?;
 
-            let video = get_video_by_hash(app, &hash).await?;
-            if video.cache_path.is_some() {
-                handle_status_change(app, shared, line_number, VideoStatus::Cached).await?;
+            let video = video_by_hash(app, &hash).await?;
+
+            if let VideoStatus::Cached {
+                cache_path,
+                is_focused,
+            } = video.status
+            {
+                handle_status_change(app, shared, line_number, VideoStatus::Cached {
+                    cache_path,
+                    is_focused,
+                })
+                .await?;
             } else {
                 handle_status_change(app, shared, line_number, VideoStatus::Watch).await?;
             }
@@ -79,16 +88,16 @@ async fn handle_status_change(
     );
     let priority = compute_priority(line_number, shared.priority);
 
-    set_video_status(app, &hash, new_status, priority).await?;
+    video_status(app, &hash, new_status, priority).await?;
     set_video_options(app, &hash, &video_options).await?;
 
     Ok(())
 }
 
-fn compute_priority(line_number: Option<i64>, priority: Option<i64>) -> Option<i64> {
+fn compute_priority(line_number: Option<i64>, priority: Option<i64>) -> Option<Priority> {
     if let Some(pri) = priority {
-        Some(pri)
+        Some(Priority::from(pri))
     } else {
-        line_number
+        line_number.map(Priority::from)
     }
 }
diff --git a/yt/src/select/mod.rs b/yt/src/select/mod.rs
index 34262af..44c8d4f 100644
--- a/yt/src/select/mod.rs
+++ b/yt/src/select/mod.rs
@@ -19,7 +19,7 @@ use crate::{
     app::App,
     cli::CliArgs,
     constants::HELP_STR,
-    storage::video_database::{Video, VideoStatus, getters::get_videos},
+    storage::video_database::{Video, VideoStatusMarker, get},
     unreachable::Unreachable,
 };
 
@@ -35,7 +35,7 @@ pub mod cmds;
 pub mod selection_file;
 
 async fn to_select_file_display_owned(video: Video, app: &App) -> Result<String> {
-    (&video).to_select_file_display(&app).await
+    video.to_select_file_display(app).await
 }
 
 pub async fn select(app: &App, done: bool, use_last_selection: bool) -> Result<()> {
@@ -50,18 +50,14 @@ pub async fn select(app: &App, done: bool, use_last_selection: bool) -> Result<(
         fs::copy(&app.config.paths.last_selection_path, &temp_file)?;
     } else {
         let matching_videos = if done {
-            get_videos(app, VideoStatus::ALL, None).await?
+            get::videos(app, VideoStatusMarker::ALL).await?
         } else {
-            get_videos(
-                app,
-                &[
-                    VideoStatus::Pick,
-                    //
-                    VideoStatus::Watch,
-                    VideoStatus::Cached,
-                ],
-                None,
-            )
+            get::videos(app, &[
+                VideoStatusMarker::Pick,
+                //
+                VideoStatusMarker::Watch,
+                VideoStatusMarker::Cached,
+            ])
             .await?
         };
 
diff --git a/yt/src/select/selection_file/duration.rs b/yt/src/select/selection_file/duration.rs
index 2953bd3..4fb3d4c 100644
--- a/yt/src/select/selection_file/duration.rs
+++ b/yt/src/select/selection_file/duration.rs
@@ -9,34 +9,66 @@
 // If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 
 use std::str::FromStr;
+use std::time::Duration;
 
 use anyhow::{Context, Result};
 
-use crate::unreachable::Unreachable;
-
 const SECOND: u64 = 1;
 const MINUTE: u64 = 60 * SECOND;
 const HOUR: u64 = 60 * MINUTE;
 const DAY: u64 = 24 * HOUR;
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
-pub struct Duration {
-    time: u64,
+pub struct MaybeDuration {
+    time: Option<Duration>,
 }
 
-impl Duration {
+impl MaybeDuration {
+    #[must_use]
+    pub fn from_std(d: Duration) -> Self {
+        Self { time: Some(d) }
+    }
+
+    #[must_use]
+    pub fn from_secs_f64(d: f64) -> Self {
+        Self {
+            time: Some(Duration::from_secs_f64(d)),
+        }
+    }
+    #[must_use]
+    pub fn from_maybe_secs_f64(d: Option<f64>) -> Self {
+        Self {
+            time: d.map(Duration::from_secs_f64),
+        }
+    }
     #[must_use]
-    pub fn from_std(d: std::time::Duration) -> Self {
-        Self { time: d.as_secs() }
+    pub fn from_secs(d: u64) -> Self {
+        Self {
+            time: Some(Duration::from_secs(d)),
+        }
     }
 
     #[must_use]
-    pub fn value(&self) -> u64 {
-        self.time
+    pub fn zero() -> Self {
+        Self {
+            time: Some(Duration::default()),
+        }
+    }
+
+    /// Try to return the current duration encoded as seconds.
+    #[must_use]
+    pub fn as_secs(&self) -> Option<u64> {
+        self.time.map(|v| v.as_secs())
+    }
+
+    /// Try to return the current duration encoded as seconds and nanoseconds.
+    #[must_use]
+    pub fn as_secs_f64(&self) -> Option<f64> {
+        self.time.map(|v| v.as_secs_f64())
     }
 }
 
-impl FromStr for Duration {
+impl FromStr for MaybeDuration {
     type Err = anyhow::Error;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
@@ -48,7 +80,7 @@ impl FromStr for Duration {
         }
 
         if s == "[No duration]" {
-            return Ok(Self { time: 0 });
+            return Ok(Self { time: None });
         }
 
         let buf: Vec<_> = s.split(' ').collect();
@@ -83,41 +115,35 @@ impl FromStr for Duration {
         }
 
         Ok(Self {
-            time: days * DAY + hours * HOUR + minutes * MINUTE + seconds * SECOND,
+            time: Some(Duration::from_secs(
+                days * DAY + hours * HOUR + minutes * MINUTE + seconds * SECOND,
+            )),
         })
     }
 }
 
-impl From<Option<f64>> for Duration {
-    fn from(value: Option<f64>) -> Self {
-        Self {
-            #[allow(clippy::cast_possible_truncation)]
-            time: u64::try_from(value.unwrap_or(0.0).ceil() as i128)
-                .unreachable("This should not exceed `u64::MAX`"),
-        }
-    }
-}
-
-impl std::fmt::Display for Duration {
+impl std::fmt::Display for MaybeDuration {
     fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
-        let base_day = self.time - (self.time % DAY);
-        let base_hour = (self.time % DAY) - ((self.time % DAY) % HOUR);
-        let base_min = (self.time % HOUR) - (((self.time % DAY) % HOUR) % MINUTE);
-        let base_sec = ((self.time % DAY) % HOUR) % MINUTE;
-
-        let d = base_day / DAY;
-        let h = base_hour / HOUR;
-        let m = base_min / MINUTE;
-        let s = base_sec / SECOND;
-
-        if self.time == 0 {
-            write!(fmt, "[No duration]")
-        } else if d > 0 {
-            write!(fmt, "{d}d {h}h {m}m")
-        } else if h > 0 {
-            write!(fmt, "{h}h {m}m")
+        if let Some(self_seconds) = self.as_secs() {
+            let base_day = self_seconds - (self_seconds % DAY);
+            let base_hour = (self_seconds % DAY) - ((self_seconds % DAY) % HOUR);
+            let base_min = (self_seconds % HOUR) - (((self_seconds % DAY) % HOUR) % MINUTE);
+            let base_sec = ((self_seconds % DAY) % HOUR) % MINUTE;
+
+            let d = base_day / DAY;
+            let h = base_hour / HOUR;
+            let m = base_min / MINUTE;
+            let s = base_sec / SECOND;
+
+            if d > 0 {
+                write!(fmt, "{d}d {h}h {m}m")
+            } else if h > 0 {
+                write!(fmt, "{h}h {m}m")
+            } else {
+                write!(fmt, "{m}m {s}s")
+            }
         } else {
-            write!(fmt, "{m}m {s}s")
+            write!(fmt, "[No duration]")
         }
     }
 }
@@ -125,23 +151,34 @@ impl std::fmt::Display for Duration {
 mod test {
     use std::str::FromStr;
 
-    use super::Duration;
+    use crate::select::selection_file::duration::{DAY, HOUR, MINUTE};
+
+    use super::MaybeDuration;
 
     #[test]
     fn test_display_duration_1h() {
-        let dur = Duration { time: 60 * 60 };
+        let dur = MaybeDuration::from_secs(HOUR);
         assert_eq!("1h 0m".to_owned(), dur.to_string());
     }
     #[test]
     fn test_display_duration_30min() {
-        let dur = Duration { time: 60 * 30 };
+        let dur = MaybeDuration::from_secs(MINUTE * 30);
         assert_eq!("30m 0s".to_owned(), dur.to_string());
     }
     #[test]
+    fn test_display_duration_1d() {
+        let dur = MaybeDuration::from_secs(DAY + MINUTE * 30 + HOUR * 2);
+        assert_eq!("1d 2h 30m".to_owned(), dur.to_string());
+    }
+
+    #[test]
     fn test_display_duration_roundtrip() {
-        let dur = Duration { time: 0 };
+        let dur = MaybeDuration::zero();
         let dur_str = dur.to_string();
 
-        assert_eq!(Duration { time: 0 }, Duration::from_str(&dur_str).unwrap());
+        assert_eq!(
+            MaybeDuration::zero(),
+            MaybeDuration::from_str(&dur_str).unwrap()
+        );
     }
 }
diff --git a/yt/src/status/mod.rs b/yt/src/status/mod.rs
index 501bcf3..9ffec27 100644
--- a/yt/src/status/mod.rs
+++ b/yt/src/status/mod.rs
@@ -11,53 +11,36 @@
 use std::time::Duration;
 
 use crate::{
-    select::selection_file::duration::Duration as YtDuration, storage::migrate::get_version,
-};
-
-use anyhow::{Context, Result};
-use bytes::Bytes;
-
-use crate::{
     app::App,
     download::Downloader,
+    select::selection_file::duration::MaybeDuration,
     storage::{
-        subscriptions::get,
-        video_database::{VideoStatus, getters::get_videos},
+        subscriptions,
+        video_database::{VideoStatusMarker, get},
     },
 };
 
+use anyhow::{Context, Result};
+use bytes::Bytes;
+
 macro_rules! get {
     ($videos:expr, $status:ident) => {
         $videos
             .iter()
-            .filter(|vid| vid.status == VideoStatus::$status)
+            .filter(|vid| vid.status.as_marker() == VideoStatusMarker::$status)
             .count()
     };
 
     (@collect $videos:expr, $status:ident) => {
         $videos
             .iter()
-            .filter(|vid| vid.status == VideoStatus::$status)
+            .filter(|vid| vid.status.as_marker() == VideoStatusMarker::$status)
             .collect()
     };
 }
 
 pub async fn show(app: &App) -> Result<()> {
-    let all_videos = get_videos(
-        app,
-        &[
-            VideoStatus::Pick,
-            //
-            VideoStatus::Watch,
-            VideoStatus::Cached,
-            VideoStatus::Watched,
-            //
-            VideoStatus::Drop,
-            VideoStatus::Dropped,
-        ],
-        None,
-    )
-    .await?;
+    let all_videos = get::videos(app, VideoStatusMarker::ALL).await?;
 
     // lengths
     let picked_videos_len = get!(all_videos, Pick);
@@ -70,39 +53,36 @@ pub async fn show(app: &App) -> Result<()> {
     let drop_videos_len = get!(all_videos, Drop);
     let dropped_videos_len = get!(all_videos, Dropped);
 
-    let subscriptions = get(app).await?;
+    let subscriptions = subscriptions::get(app).await?;
     let subscriptions_len = subscriptions.0.len();
 
     let watchtime_status = {
-        let total_watch_time_raw = YtDuration::from_std(Duration::from_secs_f64(
-            watched_videos
-                .iter()
-                .fold(0f64, |acc, vid| acc + vid.duration.unwrap_or(0f64)),
-        ));
+        let total_watch_time_raw = watched_videos
+            .iter()
+            .fold(Duration::default(), |acc, vid| acc + vid.watch_progress);
 
         // Most things are watched at a speed of s (which is defined in the config file).
         // Thus
         //      y = x * s -> y / s = x
-        #[allow(clippy::cast_precision_loss)]
-        let total_watch_time = YtDuration::from_std(Duration::from_secs_f64(
-            (total_watch_time_raw.value() as f64) / app.config.select.playback_speed,
-        ));
-
-        if total_watch_time.value() == 0 {
-            // do not display a watchtime, if it is 0
-            String::new()
+        let total_watch_time = Duration::from_secs_f64(
+            (total_watch_time_raw.as_secs_f64()) / app.config.select.playback_speed,
+        );
+
+        let speed = app.config.select.playback_speed;
+
+        // Do not print the adjusted time, if the user has keep the speed level at 1.
+        #[allow(clippy::float_cmp)]
+        if speed == 1.0 {
+            format!(
+                "Total Watchtime: {}\n",
+                MaybeDuration::from_std(total_watch_time_raw)
+            )
         } else {
-            let speed = app.config.select.playback_speed;
-
-            // Do not print the adjusted time, if the user has keep the speed level at 1.
-            #[allow(clippy::float_cmp)]
-            if speed == 1.0 {
-                format!("Total Watchtime: {total_watch_time_raw}\n")
-            } else {
-                format!(
-                    "Total Watchtime: {total_watch_time_raw} (at {speed} speed: {total_watch_time})\n",
-                )
-            }
+            format!(
+                "Total Watchtime: {} (at {speed} speed: {})\n",
+                MaybeDuration::from_std(total_watch_time_raw),
+                MaybeDuration::from_std(total_watch_time),
+            )
         }
     };
 
diff --git a/yt/src/storage/migrate/mod.rs b/yt/src/storage/migrate/mod.rs
index 9696616..ee43008 100644
--- a/yt/src/storage/migrate/mod.rs
+++ b/yt/src/storage/migrate/mod.rs
@@ -1,12 +1,13 @@
 use std::{
     fmt::Display,
+    future::Future,
     time::{SystemTime, UNIX_EPOCH},
 };
 
 use anyhow::{Context, Result, bail};
 use chrono::TimeDelta;
 use log::{debug, info};
-use sqlx::{Sqlite, Transaction, query};
+use sqlx::{Sqlite, SqlitePool, Transaction, query};
 
 use crate::app::App;
 
diff --git a/yt/src/storage/video_database/downloader.rs b/yt/src/storage/video_database/downloader.rs
index d8b2041..e843d6d 100644
--- a/yt/src/storage/video_database/downloader.rs
+++ b/yt/src/storage/video_database/downloader.rs
@@ -13,10 +13,12 @@ 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, unreachable::Unreachable, video_from_record,
+    app::App,
+    storage::video_database::{VideoStatus, VideoStatusMarker},
+    unreachable::Unreachable,
+    video_from_record,
 };
 
 use super::{ExtractorHash, Video};
@@ -27,9 +29,9 @@ use super::{ExtractorHash, Video};
 /// # Panics
 /// Only if assertions fail.
 pub async fn get_next_uncached_video(app: &App) -> Result<Option<Video>> {
-    let status = VideoStatus::Watch.as_db_integer();
+    let status = VideoStatus::Watch.as_marker().as_db_integer();
 
-    // NOTE: The ORDER BY statement should be the same as the one in [`getters::get_videos`].<2024-08-22>
+    // NOTE: The ORDER BY statement should be the same as the one in [`get::videos`].<2024-08-22>
     let result = query!(
         r#"
         SELECT *
@@ -69,7 +71,7 @@ pub async fn set_video_cache_path(
 
         let path_str = path.display().to_string();
         let extractor_hash = video.hash().to_string();
-        let status = VideoStatus::Cached.as_db_integer();
+        let status = VideoStatusMarker::Cached.as_db_integer();
 
         query!(
             r#"
@@ -92,7 +94,7 @@ pub async fn set_video_cache_path(
         );
 
         let extractor_hash = video.hash().to_string();
-        let status = VideoStatus::Watch.as_db_integer();
+        let status = VideoStatus::Watch.as_marker().as_db_integer();
 
         query!(
             r#"
diff --git a/yt/src/storage/video_database/extractor_hash.rs b/yt/src/storage/video_database/extractor_hash.rs
index d080f97..57f4f19 100644
--- a/yt/src/storage/video_database/extractor_hash.rs
+++ b/yt/src/storage/video_database/extractor_hash.rs
@@ -15,7 +15,7 @@ use blake3::Hash;
 use log::debug;
 use tokio::sync::OnceCell;
 
-use crate::{app::App, storage::video_database::getters::get_all_hashes, unreachable::Unreachable};
+use crate::{app::App, storage::video_database::get::get_all_hashes, unreachable::Unreachable};
 
 static EXTRACTOR_HASH_LENGTH: OnceCell<usize> = OnceCell::const_new();
 
diff --git a/yt/src/storage/video_database/getters.rs b/yt/src/storage/video_database/getters.rs
deleted file mode 100644
index 09cc9ee..0000000
--- a/yt/src/storage/video_database/getters.rs
+++ /dev/null
@@ -1,347 +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-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;
-use sqlx::{QueryBuilder, Row, Sqlite, query};
-use url::Url;
-use yt_dlp::wrapper::info_json::InfoJson;
-
-use crate::{
-    app::App,
-    storage::{
-        subscriptions::Subscription,
-        video_database::{InPlaylist, Video, extractor_hash::ExtractorHash},
-    },
-    unreachable::Unreachable,
-};
-
-use super::{MpvOptions, VideoOptions, VideoStatus, YtDlpOptions};
-
-#[macro_export]
-macro_rules! video_from_record {
-    ($record:expr) => {
-        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: if let Some(url) = &$record.thumbnail_url {
-                Some(Url::parse(&url).expect("Parsing this as url should always work"))
-            } else {
-                None
-            },
-            title: $record.title.clone(),
-            url: Url::parse(&$record.url).expect("Parsing this as url should always work"),
-            priority: $record.priority,
-
-            in_playlist: {
-                if $record.in_playlist == 1 && $record.is_focused == 1 {
-                    super::InPlaylist::Focused
-                } else if $record.in_playlist == 1 && $record.is_focused == 0 {
-                    super::InPlaylist::Hidden
-                } else if $record.in_playlist == 0 && $record.is_focused == 0 {
-                    super::InPlaylist::Excluded
-                } else {
-                    unreachable!("Other combinations should not exist")
-                }
-            },
-
-            watch_progress: $record.watch_progress as u32,
-        }
-    };
-}
-
-/// 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
-///
-/// # Panics
-/// Only, if assertions fail.
-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()
-                        .unreachable("The db hash should always 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).unreachable(
-                            "Parsing this as url should always work. \
-                             As it was an URL when we put it in.",
-                        )
-                    }),
-                title: base.get::<String, &str>("title").clone(),
-                url: Url::parse(base.get("url")).unreachable(
-                    "Parsing this as url should always work. \
-                    As it was an URL when we put it in.",
-                ),
-                priority: base.get("priority"),
-
-                in_playlist: {
-                    let in_playlist = base.get::<u8, &str>("in_playlist");
-                    let is_focused = base.get::<u8, &str>("is_focused");
-
-                    if in_playlist == 1 && is_focused == 1 {
-                        InPlaylist::Focused
-                    } else if in_playlist == 1 && is_focused == 0 {
-                        InPlaylist::Hidden
-                    } else if in_playlist == 0 && is_focused == 0 {
-                        InPlaylist::Excluded
-                    } else {
-                        unreachable!("Other combinations should not be possible")
-                    }
-                },
-                watch_progress: base.get::<i64, &str>("watch_progress") as u32,
-            })
-        })
-        .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?;
-
-    Ok(video_from_record! {raw_video})
-}
-
-/// # Panics
-/// Only if assertions fail.
-pub async fn get_currently_playing_video(app: &App) -> Result<Option<Video>> {
-    let record = query!("SELECT * FROM videos WHERE is_focused = 1 AND in_playlist = 1")
-        .fetch_one(&app.database)
-        .await;
-
-    if let Err(sqlx::Error::RowNotFound) = record {
-        Ok(None)
-    } else {
-        let base = record?;
-        Ok(Some(video_from_record! {base}))
-    }
-}
-
-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 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 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/yt/src/storage/video_database/mod.rs b/yt/src/storage/video_database/mod.rs
index 22628b5..34b91fe 100644
--- a/yt/src/storage/video_database/mod.rs
+++ b/yt/src/storage/video_database/mod.rs
@@ -8,52 +8,102 @@
 // 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 std::{
+    fmt::{Display, Write},
+    path::PathBuf,
+    time::Duration,
+};
 
+use chrono::{DateTime, Utc};
 use url::Url;
 
-use crate::{app::App, storage::video_database::extractor_hash::ExtractorHash};
+use crate::{
+    app::App, select::selection_file::duration::MaybeDuration,
+    storage::video_database::extractor_hash::ExtractorHash,
+};
 
 pub mod downloader;
 pub mod extractor_hash;
-pub mod getters;
-pub mod setters;
+pub mod get;
 pub mod notify;
+pub mod set;
 
 #[derive(Debug, Clone)]
 pub struct Video {
-    pub cache_path: Option<PathBuf>,
     pub description: Option<String>,
-    pub duration: Option<f64>,
+    pub duration: MaybeDuration,
     pub extractor_hash: ExtractorHash,
-    pub last_status_change: i64,
+    pub 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: i64,
-    pub publish_date: Option<i64>,
+    pub priority: Priority,
+    pub publish_date: Option<TimeStamp>,
     pub status: VideoStatus,
     pub thumbnail_url: Option<Url>,
     pub title: String,
     pub url: Url,
 
-    pub in_playlist: InPlaylist,
-
     /// The seconds the user has already watched the video
-    pub watch_progress: u32,
+    pub watch_progress: Duration,
+}
+
+/// The priority of a [`Video`].
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub 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 {
+        self.value
+    }
+}
+impl From<i64> for Priority {
+    fn from(value: i64) -> Self {
+        Self { value }
+    }
+}
+impl Display for Priority {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.value.fmt(f)
+    }
 }
 
-#[derive(Clone, Copy, Debug)]
-pub enum InPlaylist {
-    /// The video is not in the playlist.
-    Excluded,
+/// An UNIX time stamp.
+#[derive(Debug, Default, Clone, Copy)]
+pub struct TimeStamp {
+    value: i64,
+}
+impl TimeStamp {
+    /// Return the seconds since the UNIX epoch for this [`TimeStamp`].
+    #[must_use]
+    pub fn as_secs(&self) -> i64 {
+        self.value
+    }
 
-    /// The video is in the playlist, but not visible
-    Hidden,
+    /// Construct a [`TimeStamp`] from a count of seconds since the UNIX epoch.
+    #[must_use]
+    pub fn from_secs(value: i64) -> Self {
+        Self { value }
+    }
 
-    /// It is visible and focused.
-    /// Only one video should have this state.
-    Focused,
+    /// Construct a [`TimeStamp`] from the current time.
+    #[must_use]
+    pub fn from_now() -> Self {
+        Self {
+            value: Utc::now().timestamp(),
+        }
+    }
+}
+impl Display for TimeStamp {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        DateTime::from_timestamp(self.value, 0)
+            .expect("The timestamps should always be valid")
+            .format("%Y-%m-%d")
+            .fmt(f)
+    }
 }
 
 #[derive(Debug)]
@@ -107,7 +157,7 @@ pub struct YtDlpOptions {
 /// Cache                       // yt cache
 ///     |
 /// Watched                     // yt watch
-#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
+#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
 pub enum VideoStatus {
     #[default]
     Pick,
@@ -115,7 +165,10 @@ pub enum VideoStatus {
     /// The video has been select to be watched
     Watch,
     /// The video has been cached and is ready to be watched
-    Cached,
+    Cached {
+        cache_path: PathBuf,
+        is_focused: bool,
+    },
     /// The video has been watched
     Watched,
 
@@ -126,15 +179,90 @@ pub enum VideoStatus {
 }
 
 impl VideoStatus {
+    /// Reconstruct a [`VideoStatus`] for it's marker and the optional parts.
+    /// This should only be used by the db record to [`Video`] code.
+    ///
+    /// # Panics
+    /// Only if internal expectations fail.
+    #[must_use]
+    pub fn from_marker(marker: VideoStatusMarker, optional: Option<(PathBuf, bool)>) -> Self {
+        match marker {
+            VideoStatusMarker::Pick => Self::Pick,
+            VideoStatusMarker::Watch => Self::Watch,
+            VideoStatusMarker::Cached => {
+                let (cache_path, is_focused) =
+                    optional.expect("This should be some, when the video status is cached");
+                Self::Cached {
+                    cache_path,
+                    is_focused,
+                }
+            }
+            VideoStatusMarker::Watched => Self::Watched,
+            VideoStatusMarker::Drop => Self::Drop,
+            VideoStatusMarker::Dropped => Self::Dropped,
+        }
+    }
+
+    /// 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 {
+        match self {
+            VideoStatus::Pick => VideoStatusMarker::Pick,
+            VideoStatus::Watch => VideoStatusMarker::Watch,
+            VideoStatus::Cached { .. } => VideoStatusMarker::Cached,
+            VideoStatus::Watched => VideoStatusMarker::Watched,
+            VideoStatus::Drop => VideoStatusMarker::Drop,
+            VideoStatus::Dropped => VideoStatusMarker::Dropped,
+        }
+    }
+}
+
+/// Unit only variant of [`VideoStatus`]
+#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
+pub enum VideoStatusMarker {
+    #[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 VideoStatusMarker {
     pub const ALL: &'static [Self; 6] = &[
         Self::Pick,
         //
-        VideoStatus::Watch,
-        VideoStatus::Cached,
-        VideoStatus::Watched,
+        Self::Watch,
+        Self::Cached,
+        Self::Watched,
         //
-        VideoStatus::Drop,
-        VideoStatus::Dropped,
+        Self::Drop,
+        Self::Dropped,
     ];
 
     #[must_use]
@@ -142,12 +270,12 @@ impl VideoStatus {
         // 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   ",
+            Self::Pick => "pick   ",
 
-            VideoStatus::Watch | VideoStatus::Cached => "watch  ",
-            VideoStatus::Watched => "watched",
+            Self::Watch | Self::Cached => "watch  ",
+            Self::Watched => "watched",
 
-            VideoStatus::Drop | VideoStatus::Dropped => "drop   ",
+            Self::Drop | Self::Dropped => "drop   ",
         }
     }
 
@@ -156,14 +284,14 @@ impl VideoStatus {
         // These numbers should not change their mapping!
         // Oh, and keep them in sync with the SQLite check constraint.
         match self {
-            VideoStatus::Pick => 0,
+            Self::Pick => 0,
 
-            VideoStatus::Watch => 1,
-            VideoStatus::Cached => 2,
-            VideoStatus::Watched => 3,
+            Self::Watch => 1,
+            Self::Cached => 2,
+            Self::Watched => 3,
 
-            VideoStatus::Drop => 4,
-            VideoStatus::Dropped => 5,
+            Self::Drop => 4,
+            Self::Dropped => 5,
         }
     }
     #[must_use]
@@ -187,14 +315,14 @@ impl VideoStatus {
     #[must_use]
     pub fn as_str(&self) -> &'static str {
         match self {
-            VideoStatus::Pick => "Pick",
+            Self::Pick => "Pick",
 
-            VideoStatus::Watch => "Watch",
-            VideoStatus::Cached => "Cache",
-            VideoStatus::Watched => "Watched",
+            Self::Watch => "Watch",
+            Self::Cached => "Cache",
+            Self::Watched => "Watched",
 
-            VideoStatus::Drop => "Drop",
-            VideoStatus::Dropped => "Dropped",
+            Self::Drop => "Drop",
+            Self::Dropped => "Dropped",
         }
     }
 }
diff --git a/yt/src/storage/video_database/setters.rs b/yt/src/storage/video_database/setters.rs
deleted file mode 100644
index 32a745b..0000000
--- a/yt/src/storage/video_database/setters.rs
+++ /dev/null
@@ -1,317 +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, getters::get_currently_playing_video,
-    },
-};
-
-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.
-///
-/// # Panics
-/// Only if assertions fail.
-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(())
-}
-
-/// Set a video to be focused.
-/// This optionally takes the `old_video_hash` to disable.
-pub async fn set_focused(
-    app: &App,
-    new_video_hash: &ExtractorHash,
-    old_video_hash: Option<&ExtractorHash>,
-) -> Result<()> {
-    if let Some(old) = old_video_hash {
-        let hash = old.hash().to_string();
-        query!(
-            r#"
-            UPDATE videos
-            SET is_focused = 0
-            WHERE extractor_hash = ?;
-        "#,
-            hash
-        )
-        .execute(&app.database)
-        .await?;
-    }
-
-    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_playing_video(app)
-            .await?
-            .expect("This is some at this point")
-            .extractor_hash
-    );
-    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 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 (in_playlist, is_focused) = {
-        match video.in_playlist {
-            super::InPlaylist::Excluded => (false, false),
-            super::InPlaylist::Hidden => (true, false),
-            super::InPlaylist::Focused => (true, true),
-        }
-    };
-
-    let mut tx = app.database.begin().await?;
-    query!(
-        r#"
-        INSERT INTO videos (
-            description,
-            duration,
-            extractor_hash,
-            in_playlist,
-            is_focused,
-            last_status_change,
-            parent_subscription_name,
-            publish_date,
-            status,
-            thumbnail_url,
-            title,
-            url,
-            watch_progress
-            )
-        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
-    "#,
-        video.description,
-        video.duration,
-        extractor_hash,
-        in_playlist,
-        is_focused,
-        video.last_status_change,
-        parent_subscription_name,
-        video.publish_date,
-        status,
-        thumbnail_url,
-        video.title,
-        url,
-        video.watch_progress
-    )
-    .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/yt/src/update/mod.rs b/yt/src/update/mod.rs
index da19bae..c462b1e 100644
--- a/yt/src/update/mod.rs
+++ b/yt/src/update/mod.rs
@@ -8,7 +8,7 @@
 // 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::str::FromStr;
+use std::{str::FromStr, time::Duration};
 
 use anyhow::{Context, Ok, Result};
 use chrono::{DateTime, Utc};
@@ -18,11 +18,12 @@ use yt_dlp::{unsmuggle_url, wrapper::info_json::InfoJson};
 
 use crate::{
     app::App,
+    select::selection_file::duration::MaybeDuration,
     storage::{
         subscriptions::{self, Subscription},
         video_database::{
-            InPlaylist, Video, VideoStatus, extractor_hash::ExtractorHash, getters::get_all_hashes,
-            setters::add_video,
+            Priority, TimeStamp, Video, VideoStatus, extractor_hash::ExtractorHash,
+            get::get_all_hashes, set::add_video,
         },
     },
 };
@@ -160,20 +161,18 @@ pub fn video_entry_to_video(entry: InfoJson, sub: Option<&Subscription>) -> Resu
     };
 
     let video = Video {
-        cache_path: None,
         description: entry.description.clone(),
-        duration: entry.duration,
+        duration: MaybeDuration::from_maybe_secs_f64(entry.duration),
         extractor_hash: ExtractorHash::from_hash(extractor_hash),
-        last_status_change: Utc::now().timestamp(),
+        last_status_change: TimeStamp::from_now(),
         parent_subscription_name: subscription_name,
-        priority: 0,
-        publish_date,
+        priority: Priority::default(),
+        publish_date: publish_date.map(TimeStamp::from_secs),
         status: VideoStatus::Pick,
         thumbnail_url,
         title: unwrap_option!(entry.title.clone()),
         url,
-        in_playlist: InPlaylist::Excluded,
-        watch_progress: 0,
+        watch_progress: Duration::default(),
     };
     Ok(video)
 }
diff --git a/yt/src/videos/display/format_video.rs b/yt/src/videos/display/format_video.rs
index f9c50af..535a418 100644
--- a/yt/src/videos/display/format_video.rs
+++ b/yt/src/videos/display/format_video.rs
@@ -31,10 +31,10 @@ impl Video {
         let video_options = self.video_options_fmt(app).await?;
 
         let watched_percentage_fmt = {
-            if let Some(duration) = self.duration {
+            if let Some(duration) = self.duration.as_secs() {
                 format!(
-                    " (watched: {:0.1}%)",
-                    f64::from(self.watch_progress) / duration
+                    " (watched: {:0.0}%)",
+                    (self.watch_progress.as_secs() / duration) * 100
                 )
             } else {
                 format!(" {watch_progress}")
diff --git a/yt/src/videos/display/mod.rs b/yt/src/videos/display/mod.rs
index 21ab1d4..2b87add 100644
--- a/yt/src/videos/display/mod.rs
+++ b/yt/src/videos/display/mod.rs
@@ -8,16 +8,13 @@
 // 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::PathBuf;
-
-use chrono::DateTime;
 use owo_colors::OwoColorize;
 use url::Url;
 
 use crate::{
     app::App,
-    select::selection_file::duration::Duration,
-    storage::video_database::{InPlaylist, Video, getters::get_video_opts},
+    select::selection_file::duration::MaybeDuration,
+    storage::video_database::{TimeStamp, Video, VideoStatus, get::get_video_opts},
 };
 
 use anyhow::{Context, Result};
@@ -34,12 +31,6 @@ macro_rules! get {
     };
 }
 
-fn date_from_stamp(stamp: i64) -> String {
-    DateTime::from_timestamp(stamp, 0)
-        .expect("The timestamps should always be valid")
-        .format("%Y-%m-%d")
-        .to_string()
-}
 fn maybe_add_color<F>(app: &App, input: String, mut color_fn: F) -> String
 where
     F: FnMut(String) -> String,
@@ -53,12 +44,15 @@ where
 impl Video {
     #[must_use]
     pub fn cache_path_fmt(&self, app: &App) -> String {
-        let cache_path = get!(
-            self,
+        let cache_path = if let VideoStatus::Cached {
             cache_path,
-            "Cache Path",
-            (|value: &PathBuf| value.to_string_lossy().to_string())
-        );
+            is_focused: _,
+        } = &self.status
+        {
+            cache_path.to_string_lossy().to_string()
+        } else {
+            "[No Cache Path]".to_owned()
+        };
         maybe_add_color(app, cache_path, |v| v.blue().bold().to_string())
     }
 
@@ -74,7 +68,7 @@ impl Video {
 
     #[must_use]
     pub fn duration_fmt_no_color(&self) -> String {
-        Duration::from(self.duration).to_string()
+        self.duration.to_string()
     }
     #[must_use]
     pub fn duration_fmt(&self, app: &App) -> String {
@@ -84,8 +78,11 @@ impl Video {
 
     #[must_use]
     pub fn watch_progress_fmt(&self, app: &App) -> String {
-        let progress = Duration::from(Some(f64::from(self.watch_progress))).to_string();
-        maybe_add_color(app, progress, |v| v.cyan().bold().to_string())
+        maybe_add_color(
+            app,
+            MaybeDuration::from_std(self.watch_progress).to_string(),
+            |v| v.cyan().bold().to_string(),
+        )
     }
 
     pub async fn extractor_hash_fmt_no_color(&self, app: &App) -> Result<String> {
@@ -111,17 +108,27 @@ impl Video {
 
     #[must_use]
     pub fn in_playlist_fmt(&self, app: &App) -> String {
-        let output = match self.in_playlist {
-            InPlaylist::Excluded => "Not in the playlist",
-            InPlaylist::Hidden => "In the playlist",
-            InPlaylist::Focused => "In the playlist and focused",
+        let output = match &self.status {
+            VideoStatus::Pick
+            | VideoStatus::Watch
+            | VideoStatus::Watched
+            | VideoStatus::Drop
+            | VideoStatus::Dropped => "Not in the playlist",
+            VideoStatus::Cached { is_focused, .. } => {
+                if *is_focused {
+                    "In the playlist and focused"
+                } else {
+                    "In the playlist"
+                }
+            }
         };
         maybe_add_color(app, output.to_owned(), |v| v.yellow().italic().to_string())
     }
     #[must_use]
     pub fn last_status_change_fmt(&self, app: &App) -> String {
-        let lsc = date_from_stamp(self.last_status_change);
-        maybe_add_color(app, lsc, |v| v.bright_cyan().to_string())
+        maybe_add_color(app, self.last_status_change.to_string(), |v| {
+            v.bright_cyan().to_string()
+        })
     }
 
     #[must_use]
@@ -150,7 +157,7 @@ impl Video {
             self,
             publish_date,
             "release date",
-            (|date: &i64| date_from_stamp(*date))
+            (|date: &TimeStamp| date.to_string())
         )
     }
     #[must_use]
@@ -163,7 +170,7 @@ impl Video {
     pub fn status_fmt_no_color(&self) -> String {
         // TODO: We might support `.trim()`ing that, as the extra whitespace could be bad in the
         // selection file. <2024-10-07>
-        self.status.as_command().to_string()
+        self.status.as_marker().as_command().to_string()
     }
     #[must_use]
     pub fn status_fmt(&self, app: &App) -> String {
diff --git a/yt/src/videos/mod.rs b/yt/src/videos/mod.rs
index 2f9d8af..2860ad4 100644
--- a/yt/src/videos/mod.rs
+++ b/yt/src/videos/mod.rs
@@ -19,15 +19,15 @@ pub mod display;
 
 use crate::{
     app::App,
-    storage::video_database::{Video, VideoStatus, getters::get_videos},
+    storage::video_database::{Video, VideoStatusMarker, get},
 };
 
 async fn to_line_display_owned(video: Video, app: &App) -> Result<String> {
-    (&video).to_line_display(&app).await
+    video.to_line_display(app).await
 }
 
 pub async fn query(app: &App, limit: Option<usize>, search_query: Option<String>) -> Result<()> {
-    let all_videos = get_videos(app, VideoStatus::ALL, None).await?;
+    let all_videos = get::videos(app, VideoStatusMarker::ALL).await?;
 
     // turn one video to a color display, to pre-warm the hash shrinking cache
     if let Some(val) = all_videos.first() {
diff --git a/yt/src/watch/events/handlers/mod.rs b/yt/src/watch/events/handlers/mod.rs
deleted file mode 100644
index 8d4304b..0000000
--- a/yt/src/watch/events/handlers/mod.rs
+++ /dev/null
@@ -1,194 +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::{env::current_exe, mem};
-
-use crate::{app::App, comments, description, storage::video_database::setters::set_state_change};
-
-use super::MpvEventHandler;
-
-use anyhow::{Context, Result, bail};
-use libmpv2::{
-    Mpv,
-    events::{EndFileEvent, PlaylistEntryId},
-};
-use log::info;
-use tokio::process::Command;
-
-impl MpvEventHandler {
-    // EndFile {{{
-    pub async fn handle_end_file_eof(
-        &mut self,
-        app: &App,
-        mpv: &Mpv,
-        end_file_event: EndFileEvent,
-    ) -> Result<()> {
-        info!("Mpv reached eof of current video. Marking it inactive.");
-
-        self.mark_video_inactive(app, mpv, end_file_event.playlist_entry_id)
-            .await?;
-
-        Ok(())
-    }
-    pub async fn handle_end_file_stop(
-        &mut self,
-        app: &App,
-        mpv: &Mpv,
-        end_file_event: EndFileEvent,
-    ) -> Result<()> {
-        // This reason is incredibly ambiguous. It _both_ means actually pausing a
-        // video and going to the next one in the playlist.
-        // Oh, and it's also called, when a video is removed from the playlist (at
-        // least via "playlist-remove current")
-        info!("Paused video (or went to next playlist entry); Marking it inactive");
-
-        self.mark_video_inactive(app, mpv, end_file_event.playlist_entry_id)
-            .await?;
-
-        Ok(())
-    }
-    pub async fn handle_end_file_quit(
-        &mut self,
-        app: &App,
-        mpv: &Mpv,
-        _end_file_event: EndFileEvent,
-    ) -> Result<()> {
-        info!("Mpv quit. Exiting playback");
-
-        // draining the playlist is okay, as mpv is done playing
-        let mut handler = mem::take(&mut self.playlist_handler);
-        let videos = handler.playlist_ids(mpv)?;
-        for hash in videos.values() {
-            self.mark_video_watched(app, hash).await?;
-            set_state_change(app, hash, false).await?;
-        }
-
-        Ok(())
-    }
-    // }}}
-
-    // StartFile {{{
-    pub async fn handle_start_file(
-        &mut self,
-        app: &App,
-        mpv: &Mpv,
-        entry_id: PlaylistEntryId,
-    ) -> Result<()> {
-        self.possibly_add_new_videos(app, mpv, false).await?;
-
-        // We don't need to check, whether other videos are still active, as they should
-        // have been marked inactive in the `Stop` handler.
-        self.mark_video_active(app, mpv, entry_id).await?;
-        let hash = self.get_cvideo_hash(mpv, 0)?;
-        self.apply_options(app, mpv, &hash).await?;
-
-        Ok(())
-    }
-    // }}}
-
-    // ClientMessage {{{
-    async fn run_self_in_external_command(app: &App, args: &[&str]) -> Result<()> {
-        let binary =
-            current_exe().context("Failed to determine the current executable to re-execute")?;
-
-        let status = Command::new("riverctl")
-            .args(["focus-output", "next"])
-            .status()
-            .await?;
-        if !status.success() {
-            bail!("focusing the next output failed!");
-        }
-
-        let arguments = [
-            &[
-                "--title",
-                "floating please",
-                "--command",
-                binary
-                    .to_str()
-                    .context("Failed to turn the executable path to a utf8-string")?,
-                "--db-path",
-                app.config
-                    .paths
-                    .database_path
-                    .to_str()
-                    .context("Failed to parse the database_path as a utf8-string")?,
-            ],
-            args,
-        ]
-        .concat();
-
-        let status = Command::new("alacritty").args(arguments).status().await?;
-        if !status.success() {
-            bail!("Falied to start `yt comments`");
-        }
-
-        let status = Command::new("riverctl")
-            .args(["focus-output", "next"])
-            .status()
-            .await?;
-
-        if !status.success() {
-            bail!("focusing the next output failed!");
-        }
-
-        Ok(())
-    }
-
-    pub async fn handle_client_message_yt_description_external(app: &App) -> Result<()> {
-        Self::run_self_in_external_command(app, &["description"]).await?;
-        Ok(())
-    }
-    pub async fn handle_client_message_yt_description_local(app: &App, mpv: &Mpv) -> Result<()> {
-        let description: String = description::get(app)
-            .await?
-            .replace(['"', '\''], "")
-            .chars()
-            .take(app.config.watch.local_displays_length)
-            .collect();
-
-        Self::message(mpv, &description, "6000")?;
-        Ok(())
-    }
-
-    pub async fn handle_client_message_yt_comments_external(app: &App) -> Result<()> {
-        Self::run_self_in_external_command(app, &["comments"]).await?;
-        Ok(())
-    }
-    pub async fn handle_client_message_yt_comments_local(app: &App, mpv: &Mpv) -> Result<()> {
-        let comments: String = comments::get(app)
-            .await?
-            .render(false)
-            .replace(['"', '\''], "")
-            .chars()
-            .take(app.config.watch.local_displays_length)
-            .collect();
-
-        Self::message(mpv, &comments, "6000")?;
-        Ok(())
-    }
-
-    /// # Panics
-    /// Only if internal assertions fail.
-    pub fn handle_client_message_yt_mark_watch_later(&mut self, mpv: &Mpv) -> Result<()> {
-        mpv.command("write-watch-later-config", &[])?;
-
-        let hash = self.remove_cvideo_from_playlist(mpv)?;
-        assert!(
-            self.watch_later_block_list.insert(hash),
-            "A video should not be blocked *and* in the playlist"
-        );
-
-        Self::message(mpv, "Marked the video to be watched later", "3000")?;
-
-        Ok(())
-    }
-    // }}}
-}
diff --git a/yt/src/watch/events/mod.rs b/yt/src/watch/events/mod.rs
deleted file mode 100644
index 7a08610..0000000
--- a/yt/src/watch/events/mod.rs
+++ /dev/null
@@ -1,322 +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, HashSet};
-
-use anyhow::{Context, Result};
-use libmpv2::{
-    EndFileReason, Mpv,
-    events::{Event, PlaylistEntryId},
-};
-use log::{debug, info};
-
-use crate::{
-    app::App,
-    storage::video_database::{
-        VideoStatus,
-        extractor_hash::ExtractorHash,
-        getters::{get_video_by_hash, get_video_mpv_opts, get_videos},
-        setters::{set_state_change, set_video_watched},
-    },
-    unreachable::Unreachable,
-};
-
-use playlist_handler::PlaylistHandler;
-
-mod handlers;
-mod playlist_handler;
-
-#[derive(Debug, Clone, Copy)]
-pub enum IdleCheckOutput {
-    /// There are no videos already downloaded and no more marked to be watched.
-    /// Waiting is pointless.
-    NoMoreAvailable,
-
-    /// There are no videos cached, but some (>0) are marked to be watched.
-    /// So we should wait for them to become available.
-    NoCached { marked_watched: usize },
-
-    /// There are videos cached and ready to be inserted into the playback queue.
-    Available { newly_available: Option<usize> },
-}
-
-#[derive(Debug)]
-pub struct MpvEventHandler {
-    watch_later_block_list: HashSet<ExtractorHash>,
-    playlist_handler: PlaylistHandler,
-}
-
-impl MpvEventHandler {
-    #[must_use]
-    pub fn from_playlist(playlist_cache: HashMap<String, ExtractorHash>) -> Self {
-        let playlist_handler = PlaylistHandler::from_cache(playlist_cache);
-        Self {
-            playlist_handler,
-            watch_later_block_list: HashSet::new(),
-        }
-    }
-
-    /// Checks, whether new videos are ready to be played
-    pub async fn possibly_add_new_videos(
-        &mut self,
-        app: &App,
-        mpv: &Mpv,
-        force_message: bool,
-    ) -> Result<usize> {
-        let play_things = get_videos(app, &[VideoStatus::Cached], Some(false)).await?;
-
-        // There is nothing to watch
-        if play_things.is_empty() {
-            if force_message {
-                Self::message(mpv, "No new videos available to add", "3000")?;
-            }
-            return Ok(0);
-        }
-
-        let mut blocked_videos = 0;
-        let current_playlist = self.playlist_handler.playlist_ids(mpv)?;
-        let play_things = play_things
-            .into_iter()
-            .filter(|val| !current_playlist.values().any(|a| a == &val.extractor_hash))
-            .filter(|val| {
-                if self.watch_later_block_list.contains(&val.extractor_hash) {
-                    blocked_videos += 1;
-                    false
-                } else {
-                    true
-                }
-            })
-            .collect::<Vec<_>>();
-
-        info!(
-            "{} videos are cached and will be added to the list to be played ({} are blocked)",
-            play_things.len(),
-            blocked_videos
-        );
-
-        let num = play_things.len();
-        self.playlist_handler.reserve(play_things.len());
-        for play_thing in play_things {
-            debug!("Adding '{}' to playlist.", play_thing.title);
-
-            let orig_cache_path = play_thing.cache_path.unreachable("Is cached and thus some");
-            let cache_path = orig_cache_path.to_str().with_context(|| {
-                format!(
-                    "Failed to parse video cache_path as vaild utf8: '{}'",
-                    orig_cache_path.display()
-                )
-            })?;
-
-            mpv.command("loadfile", &[cache_path, "append-play"])?;
-            self.playlist_handler
-                .add(cache_path.to_owned(), play_thing.extractor_hash);
-        }
-
-        if force_message || num > 0 {
-            Self::message(
-                mpv,
-                format!("Added {num} videos ({blocked_videos} are marked as watch later)").as_str(),
-                "3000",
-            )?;
-        }
-        Ok(num)
-    }
-
-    fn message(mpv: &Mpv, message: &str, time: &str) -> Result<()> {
-        mpv.command("show-text", &[message, time])?;
-        Ok(())
-    }
-
-    /// Get the hash of the currently playing video.
-    /// You can specify an offset, which is added to the ``playlist_position`` to get, for example, the
-    /// previous video (-1) or the next video (+1).
-    /// Beware that setting an offset can cause an property error if it's out of bound.
-    fn get_cvideo_hash(&mut self, mpv: &Mpv, offset: i64) -> Result<ExtractorHash> {
-        let playlist_entry_id = {
-            let playlist_position = {
-                let raw = mpv.get_property::<i64>("playlist-pos")?;
-                if raw == -1 {
-                    unreachable!(
-                        "Tried to get the currently playing video hash, but failed to access the mpv 'playlist-pos' property! This is a bug, as this function should only be called, when a current video exists. Current state: '{:#?}'",
-                        self
-                    );
-                } else {
-                    usize::try_from(raw + offset).with_context(|| format!("Failed to calculate playlist position because of usize overflow: '{raw} + {offset}'"))?
-                }
-            };
-
-            let raw =
-                mpv.get_property::<i64>(format!("playlist/{playlist_position}/id").as_str())?;
-            PlaylistEntryId::new(raw)
-        };
-
-        // debug!("Trying to get playlist entry: '{}'", playlist_entry_id);
-
-        let video_hash = self
-            .playlist_handler
-            .playlist_ids(mpv)?
-            .get(&playlist_entry_id)
-            .expect("The stored playling index should always be in the playlist")
-            .to_owned();
-
-        Ok(video_hash)
-    }
-    async fn mark_video_watched(&self, app: &App, hash: &ExtractorHash) -> Result<()> {
-        let video = get_video_by_hash(app, hash).await?;
-        debug!("MPV handler will mark video '{}' watched.", video.title);
-        set_video_watched(app, &video).await?;
-        Ok(())
-    }
-
-    async fn mark_video_inactive(
-        &mut self,
-        app: &App,
-        mpv: &Mpv,
-        playlist_index: PlaylistEntryId,
-    ) -> Result<()> {
-        let current_playlist = self.playlist_handler.playlist_ids(mpv)?;
-        let video_hash = current_playlist
-            .get(&playlist_index)
-            .expect("The video index should always be correctly tracked");
-
-        set_state_change(app, video_hash, false).await?;
-        Ok(())
-    }
-    async fn mark_video_active(
-        &mut self,
-        app: &App,
-        mpv: &Mpv,
-        playlist_index: PlaylistEntryId,
-    ) -> Result<()> {
-        let current_playlist = self.playlist_handler.playlist_ids(mpv)?;
-        let video_hash = current_playlist
-            .get(&playlist_index)
-            .expect("The video index should always be correctly tracked");
-
-        set_state_change(app, video_hash, true).await?;
-        Ok(())
-    }
-
-    /// Apply the options set with e.g. `watch --speed=<speed>`
-    async fn apply_options(&self, app: &App, mpv: &Mpv, hash: &ExtractorHash) -> Result<()> {
-        let options = get_video_mpv_opts(app, hash).await?;
-
-        mpv.set_property("speed", options.playback_speed)?;
-        Ok(())
-    }
-
-    /// This also returns the hash of the current video
-    fn remove_cvideo_from_playlist(&mut self, mpv: &Mpv) -> Result<ExtractorHash> {
-        let hash = self.get_cvideo_hash(mpv, 0)?;
-        mpv.command("playlist-remove", &["current"])?;
-        Ok(hash)
-    }
-
-    /// Check if the playback queue is empty
-    pub async fn check_idle(&mut self, app: &App, mpv: &Mpv) -> Result<IdleCheckOutput> {
-        if mpv.get_property::<bool>("idle-active")? {
-            // The playback is currently not running, but we might still have more videos lined up
-            // to be inserted into the queue.
-
-            let number_of_new_videos = self.possibly_add_new_videos(app, mpv, false).await?;
-
-            if number_of_new_videos == 0 {
-                let watch_videos = get_videos(app, &[VideoStatus::Watch], None).await?.len();
-
-                if watch_videos == 0 {
-                    // There are no more videos left. We should exit now.
-                    Ok(IdleCheckOutput::NoMoreAvailable)
-                } else {
-                    // There are still videos that *could* get downloaded. Wait for them.
-                    Ok(IdleCheckOutput::NoCached {
-                        marked_watched: watch_videos,
-                    })
-                }
-            } else {
-                Ok(IdleCheckOutput::Available {
-                    newly_available: Some(number_of_new_videos),
-                })
-            }
-        } else {
-            // The playback is running. Obviously, something is available.
-            Ok(IdleCheckOutput::Available {
-                newly_available: None,
-            })
-        }
-    }
-
-    /// This will return [`true`], if the event handling should be stopped
-    pub async fn handle_mpv_event(
-        &mut self,
-        app: &App,
-        mpv: &Mpv,
-        event: Event<'_>,
-    ) -> Result<bool> {
-        match event {
-            Event::EndFile(r) => match r.reason {
-                EndFileReason::Eof => {
-                    self.handle_end_file_eof(app, mpv, r).await?;
-                }
-                EndFileReason::Stop => {
-                    self.handle_end_file_stop(app, mpv, r).await?;
-                }
-                EndFileReason::Quit => {
-                    self.handle_end_file_quit(app, mpv, r).await?;
-                    return Ok(true);
-                }
-                EndFileReason::Error => {
-                    unreachable!("This will be raised as a separate error")
-                }
-                EndFileReason::Redirect => {
-                    todo!("We probably need to handle this somehow");
-                }
-            },
-            Event::StartFile(entry_id) => {
-                self.handle_start_file(app, mpv, entry_id).await?;
-            }
-            Event::ClientMessage(a) => {
-                debug!("Got Client Message event: '{}'", a.join(" "));
-
-                match a.as_slice() {
-                    &["yt-comments-external"] => {
-                        Self::handle_client_message_yt_comments_external(app).await?;
-                    }
-                    &["yt-comments-local"] => {
-                        Self::handle_client_message_yt_comments_local(app, mpv).await?;
-                    }
-                    &["yt-description-external"] => {
-                        Self::handle_client_message_yt_description_external(app).await?;
-                    }
-                    &["yt-description-local"] => {
-                        Self::handle_client_message_yt_description_local(app, mpv).await?;
-                    }
-                    &["yt-mark-watch-later"] => {
-                        self.handle_client_message_yt_mark_watch_later(mpv)?;
-                    }
-                    &["yt-mark-done-and-go-next"] => {
-                        let cvideo_hash = self.remove_cvideo_from_playlist(mpv)?;
-                        self.mark_video_watched(app, &cvideo_hash).await?;
-
-                        Self::message(mpv, "Marked the video watched", "3000")?;
-                    }
-                    &["yt-check-new-videos"] => {
-                        self.possibly_add_new_videos(app, mpv, true).await?;
-                    }
-                    other => {
-                        debug!("Unknown message: {}", other.join(" "));
-                    }
-                }
-            }
-            _ => {}
-        }
-
-        Ok(false)
-    }
-}
diff --git a/yt/src/watch/events/playlist_handler.rs b/yt/src/watch/events/playlist_handler.rs
deleted file mode 100644
index 8565ea8..0000000
--- a/yt/src/watch/events/playlist_handler.rs
+++ /dev/null
@@ -1,97 +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;
-
-use anyhow::Result;
-use libmpv2::{Mpv, events::PlaylistEntryId, mpv_node::MpvNode};
-
-use crate::storage::video_database::extractor_hash::ExtractorHash;
-
-#[derive(Debug, Default)]
-pub(crate) struct PlaylistHandler {
-    /// A map of the original file paths to the videos extractor hashes.
-    /// Used to get the extractor hash from a video returned by mpv
-    playlist_cache: HashMap<String, ExtractorHash>,
-
-    /// A map of the `playlist_entry_id` field to their corresponding extractor hashes.
-    playlist_ids: HashMap<PlaylistEntryId, ExtractorHash>,
-}
-impl PlaylistHandler {
-    pub(crate) fn from_cache(cache: HashMap<String, ExtractorHash>) -> Self {
-        Self {
-            playlist_cache: cache,
-            playlist_ids: HashMap::new(),
-        }
-    }
-
-    pub(crate) fn reserve(&mut self, len: usize) {
-        self.playlist_cache.reserve(len);
-    }
-    pub(crate) fn add(&mut self, cache_path: String, extractor_hash: ExtractorHash) {
-        assert_eq!(
-            self.playlist_cache.insert(cache_path, extractor_hash),
-            None,
-            "Only new video should ever be added"
-        );
-    }
-
-    pub(crate) fn playlist_ids(
-        &mut self,
-        mpv: &Mpv,
-    ) -> Result<&HashMap<PlaylistEntryId, ExtractorHash>> {
-        let mpv_playlist: Vec<(String, PlaylistEntryId)> = match mpv.get_property("playlist")? {
-            MpvNode::ArrayIter(array) => array
-                .map(|val| match val {
-                    MpvNode::MapIter(map) => {
-                        struct BuildPlaylistEntry {
-                            filename: Option<String>,
-                            id: Option<PlaylistEntryId>,
-                        }
-                        let mut entry = BuildPlaylistEntry {
-                            filename: None,
-                            id: None,
-                        };
-
-                        map.for_each(|(key, value)| match key.as_str() {
-                            "filename" => {
-                                entry.filename = Some(value.str().expect("work").to_owned());
-                            }
-                            "id" => {
-                                entry.id = Some(PlaylistEntryId::new(value.i64().expect("Works")));
-                            }
-                            _ => (),
-                        });
-                        (entry.filename.expect("is some"), entry.id.expect("is some"))
-                    }
-                    _ => unreachable!(),
-                })
-                .collect(),
-            _ => unreachable!(),
-        };
-
-        let mut playlist: HashMap<PlaylistEntryId, ExtractorHash> =
-            HashMap::with_capacity(mpv_playlist.len());
-        for (path, key) in mpv_playlist {
-            let hash = self
-                .playlist_cache
-                .get(&path)
-                .expect("All path should also be stored in the cache")
-                .to_owned();
-            playlist.insert(key, hash);
-        }
-
-        for (id, hash) in playlist {
-            self.playlist_ids.entry(id).or_insert(hash);
-        }
-
-        Ok(&self.playlist_ids)
-    }
-}
diff --git a/yt/src/watch/mod.rs b/yt/src/watch/mod.rs
index 630de68..16d4899 100644
--- a/yt/src/watch/mod.rs
+++ b/yt/src/watch/mod.rs
@@ -8,27 +8,31 @@
 // 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, time::Duration};
+use std::{
+    sync::{
+        Arc,
+        atomic::{AtomicBool, Ordering},
+    },
+    time::Duration,
+};
 
 use anyhow::{Context, Result};
-use events::{IdleCheckOutput, MpvEventHandler};
 use libmpv2::{Mpv, events::EventContext};
 use log::{debug, info, trace, warn};
-use tokio::time;
+use playlist_handler::{reload_mpv_playlist, save_watch_progress};
+use tokio::{task, time::sleep};
 
+use self::playlist_handler::Status;
 use crate::{
     app::App,
     cache::maintain,
-    storage::video_database::{VideoStatus, extractor_hash::ExtractorHash, getters::get_videos},
-    unreachable::Unreachable,
+    storage::video_database::{get, notify::wait_for_db_write},
 };
 
-pub mod events;
-
-#[allow(clippy::too_many_lines)]
-pub async fn watch(app: &App) -> Result<()> {
-    maintain(app, false).await?;
+pub mod playlist;
+pub mod playlist_handler;
 
+fn init_mpv(app: &App) -> Result<(Mpv, EventContext)> {
     // set some default values, to make things easier (these can be overridden by the config file,
     // which we load later)
     let mpv = Mpv::with_initializer(|mpv| {
@@ -76,72 +80,82 @@ pub async fn watch(app: &App) -> Result<()> {
         );
     }
 
-    let mut ev_ctx = EventContext::new(mpv.ctx);
+    let ev_ctx = EventContext::new(mpv.ctx);
     ev_ctx.disable_deprecated_events()?;
 
-    let play_things = get_videos(app, &[VideoStatus::Cached], Some(false)).await?;
-    info!(
-        "{} videos are cached and ready to be played",
-        play_things.len()
-    );
+    Ok((mpv, ev_ctx))
+}
 
-    let mut playlist_cache: HashMap<String, ExtractorHash> =
-        HashMap::with_capacity(play_things.len());
+pub async fn watch(app: Arc<App>) -> Result<()> {
+    maintain(&app, false).await?;
 
-    for play_thing in play_things {
-        debug!("Adding '{}' to playlist.", play_thing.title);
+    let (mpv, mut ev_ctx) = init_mpv(&app).context("Failed to initialize mpv instance")?;
+    let mpv = Arc::new(mpv);
+    reload_mpv_playlist(&app, &mpv, None, None).await?;
 
-        let orig_cache_path = play_thing.cache_path.unreachable("Is cached and thus some");
-        let cache_path = orig_cache_path.to_str().with_context(|| {
-            format!(
-                "Failed to parse the cache_path of a video as utf8: '{}'",
-                orig_cache_path.display()
-            )
-        })?;
+    let should_break = Arc::new(AtomicBool::new(false));
 
-        mpv.command("loadfile", &[&cache_path, "append-play"])?;
+    let local_app = Arc::clone(&app);
+    let local_mpv = Arc::clone(&mpv);
+    let local_should_break = Arc::clone(&should_break);
+    let progress_handle = task::spawn(async move {
+        loop {
+            if local_should_break.load(Ordering::Relaxed) {
+                break;
+            }
 
-        playlist_cache.insert(cache_path.to_owned(), play_thing.extractor_hash);
-    }
+            if get::currently_focused_video(&local_app).await?.is_some() {
+                save_watch_progress(&local_app, &local_mpv).await?;
+            }
+
+            sleep(Duration::from_secs(30)).await;
+        }
+
+        Ok::<(), anyhow::Error>(())
+    });
 
-    let mut mpv_event_handler = MpvEventHandler::from_playlist(playlist_cache);
     let mut have_warned = (false, 0);
     'watchloop: loop {
-        'waitloop: while let Ok(value) = mpv_event_handler.check_idle(app, &mpv).await {
+        'waitloop: while let Ok(value) = playlist_handler::status(&app).await {
             match value {
-                IdleCheckOutput::NoMoreAvailable => {
+                Status::NoMoreAvailable => {
                     break 'watchloop;
                 }
-                IdleCheckOutput::NoCached { marked_watched } => {
+                Status::NoCached { marked_watch } => {
                     // try again next time.
                     if have_warned.0 {
-                        if have_warned.1 != marked_watched {
-                            warn!("Now {} videos are marked as watched.", marked_watched);
-                            have_warned.1 = marked_watched;
+                        if have_warned.1 != marked_watch {
+                            warn!("Now {} videos are marked as to be watched.", marked_watch);
+                            have_warned.1 = marked_watch;
                         }
                     } else {
                         warn!(
                             "There is nothing to watch yet, but still {} videos marked as to be watched. \
                         Will idle, until they become available",
-                            marked_watched
+                            marked_watch
                         );
-                        have_warned = (true, marked_watched);
+                        have_warned = (true, marked_watch);
                     }
-                    time::sleep(Duration::from_secs(10)).await;
+                    wait_for_db_write(&app).await?;
                 }
-                IdleCheckOutput::Available { newly_available: _ } => {
+                Status::Available { newly_available } => {
+                    debug!("Check and found {newly_available} videos!");
                     have_warned.0 = false;
+
                     // Something just became available!
                     break 'waitloop;
                 }
             }
         }
 
-        if let Some(ev) = ev_ctx.wait_event(600.) {
+        if let Some(ev) = ev_ctx.wait_event(30.) {
             match ev {
                 Ok(event) => {
                     trace!("Mpv event triggered: {:#?}", event);
-                    if mpv_event_handler.handle_mpv_event(app, &mpv, event).await? {
+                    if playlist_handler::handle_mpv_event(&app, &mpv, &event)
+                        .await
+                        .with_context(|| format!("Failed to handle mpv event: '{event:#?}'"))?
+                    {
                         break;
                     }
                 }
@@ -150,5 +164,8 @@ pub async fn watch(app: &App) -> Result<()> {
         }
     }
 
+    should_break.store(true, Ordering::Relaxed);
+    progress_handle.await??;
+
     Ok(())
 }