diff options
Diffstat (limited to 'crates/yt/src/storage/video_database/mod.rs')
-rw-r--r-- | crates/yt/src/storage/video_database/mod.rs | 329 |
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", + } + } +} |