about summary refs log tree commit diff stats
path: root/crates/yt/src/storage/video_database/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/yt/src/storage/video_database/mod.rs')
-rw-r--r--crates/yt/src/storage/video_database/mod.rs329
1 files changed, 329 insertions, 0 deletions
diff --git a/crates/yt/src/storage/video_database/mod.rs b/crates/yt/src/storage/video_database/mod.rs
new file mode 100644
index 0000000..74d09f0
--- /dev/null
+++ b/crates/yt/src/storage/video_database/mod.rs
@@ -0,0 +1,329 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::{
+    fmt::{Display, Write},
+    path::PathBuf,
+    time::Duration,
+};
+
+use chrono::{DateTime, Utc};
+use url::Url;
+
+use crate::{
+    app::App, select::selection_file::duration::MaybeDuration,
+    storage::video_database::extractor_hash::ExtractorHash,
+};
+
+pub mod downloader;
+pub mod extractor_hash;
+pub mod get;
+pub mod notify;
+pub mod set;
+
+#[derive(Debug, Clone)]
+pub struct Video {
+    pub description: Option<String>,
+    pub duration: MaybeDuration,
+    pub extractor_hash: ExtractorHash,
+    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: Priority,
+    pub publish_date: Option<TimeStamp>,
+    pub status: VideoStatus,
+    pub thumbnail_url: Option<Url>,
+    pub title: String,
+    pub url: Url,
+
+    /// The seconds the user has already watched the video
+    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)
+    }
+}
+
+/// 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
+    }
+
+    /// Construct a [`TimeStamp`] from a count of seconds since the UNIX epoch.
+    #[must_use]
+    pub fn from_secs(value: i64) -> Self {
+        Self { value }
+    }
+
+    /// 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)]
+pub struct VideoOptions {
+    pub yt_dlp: YtDlpOptions,
+    pub mpv: MpvOptions,
+}
+impl VideoOptions {
+    pub(crate) fn new(subtitle_langs: String, playback_speed: f64) -> Self {
+        let yt_dlp = YtDlpOptions { subtitle_langs };
+        let mpv = MpvOptions { playback_speed };
+        Self { yt_dlp, mpv }
+    }
+
+    /// This will write out the options that are different from the defaults.
+    /// Beware, that this does not set the priority.
+    #[must_use]
+    pub fn to_cli_flags(self, app: &App) -> String {
+        let mut f = String::new();
+
+        if (self.mpv.playback_speed - app.config.select.playback_speed).abs() > f64::EPSILON {
+            write!(f, " --speed '{}'", self.mpv.playback_speed).expect("Works");
+        }
+        if self.yt_dlp.subtitle_langs != app.config.select.subtitle_langs {
+            write!(f, " --subtitle-langs '{}'", self.yt_dlp.subtitle_langs).expect("Works");
+        }
+
+        f.trim().to_owned()
+    }
+}
+
+#[derive(Debug, Clone, Copy)]
+/// Additionally settings passed to mpv on watch
+pub struct MpvOptions {
+    /// The playback speed. (1 is 100%, 2.7 is 270%, and so on)
+    pub playback_speed: f64,
+}
+
+#[derive(Debug)]
+/// Additionally configuration options, passed to yt-dlp on download
+pub struct YtDlpOptions {
+    /// In the form of `lang1,lang2,lang3` (e.g. `en,de,sv`)
+    pub subtitle_langs: String,
+}
+
+/// # Video Lifetime (words in <brackets> are commands):
+///      <Pick>
+///     /    \
+/// <Watch>   <Drop> -> Dropped // yt select
+///     |
+/// Cache                       // yt cache
+///     |
+/// Watched                     // yt watch
+#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
+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 {
+        cache_path: PathBuf,
+        is_focused: bool,
+    },
+    /// The video has been watched
+    Watched,
+
+    /// The video has been select to be dropped
+    Drop,
+    /// The video has been dropped
+    Dropped,
+}
+
+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,
+        //
+        Self::Watch,
+        Self::Cached,
+        Self::Watched,
+        //
+        Self::Drop,
+        Self::Dropped,
+    ];
+
+    #[must_use]
+    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 {
+            Self::Pick => "pick   ",
+
+            Self::Watch | Self::Cached => "watch  ",
+            Self::Watched => "watched",
+
+            Self::Drop | Self::Dropped => "drop   ",
+        }
+    }
+
+    #[must_use]
+    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 {
+            Self::Pick => 0,
+
+            Self::Watch => 1,
+            Self::Cached => 2,
+            Self::Watched => 3,
+
+            Self::Drop => 4,
+            Self::Dropped => 5,
+        }
+    }
+    #[must_use]
+    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
+            ),
+        }
+    }
+
+    #[must_use]
+    pub fn as_str(&self) -> &'static str {
+        match self {
+            Self::Pick => "Pick",
+
+            Self::Watch => "Watch",
+            Self::Cached => "Cache",
+            Self::Watched => "Watched",
+
+            Self::Drop => "Drop",
+            Self::Dropped => "Dropped",
+        }
+    }
+}