From 9e8657c9762dbb66f3322976606a1b4334d45a6b Mon Sep 17 00:00:00 2001 From: Benedikt Peetz Date: Sat, 22 Feb 2025 11:29:38 +0100 Subject: feat(yt/): Use concrete types in the `Video` structure --- yt/src/cache/mod.rs | 47 ++-- yt/src/cli.rs | 4 +- yt/src/comments/mod.rs | 18 +- yt/src/download/mod.rs | 2 +- yt/src/main.rs | 4 +- yt/src/select/cmds/add.rs | 2 +- yt/src/select/cmds/mod.rs | 29 +- yt/src/select/mod.rs | 22 +- yt/src/select/selection_file/duration.rs | 127 ++++++--- yt/src/status/mod.rs | 82 +++--- yt/src/storage/migrate/mod.rs | 3 +- yt/src/storage/video_database/downloader.rs | 14 +- yt/src/storage/video_database/extractor_hash.rs | 2 +- yt/src/storage/video_database/getters.rs | 347 ------------------------ yt/src/storage/video_database/mod.rs | 216 ++++++++++++--- yt/src/storage/video_database/setters.rs | 317 ---------------------- yt/src/update/mod.rs | 19 +- yt/src/videos/display/format_video.rs | 6 +- yt/src/videos/display/mod.rs | 61 +++-- yt/src/videos/mod.rs | 6 +- yt/src/watch/events/handlers/mod.rs | 194 ------------- yt/src/watch/events/mod.rs | 322 ---------------------- yt/src/watch/events/playlist_handler.rs | 97 ------- yt/src/watch/mod.rs | 103 ++++--- 24 files changed, 474 insertions(+), 1570 deletions(-) delete mode 100644 yt/src/storage/video_database/getters.rs delete mode 100644 yt/src/storage/video_database/setters.rs delete mode 100644 yt/src/watch/events/handlers/mod.rs delete mode 100644 yt/src/watch/events/mod.rs delete mode 100644 yt/src/watch/events/playlist_handler.rs 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, - pub duration: Option, + pub duration: Option, pub url: Option, } 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 { 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(¤tly_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(¤tly_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, priority: Option) -> Option { +fn compute_priority(line_number: Option, priority: Option) -> Option { 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 { - (&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 . 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, } -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) -> 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 { + 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 { + 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 { @@ -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> for Duration { - fn from(value: Option) -> 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 @@ -10,54 +10,37 @@ 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> { - 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 = 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 -// 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 . - -//! 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, -) -> Result> { - 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