diff options
Diffstat (limited to 'src/storage/video_database/getters.rs')
-rw-r--r-- | src/storage/video_database/getters.rs | 339 |
1 files changed, 339 insertions, 0 deletions
diff --git a/src/storage/video_database/getters.rs b/src/storage/video_database/getters.rs new file mode 100644 index 0000000..ca4164d --- /dev/null +++ b/src/storage/video_database/getters.rs @@ -0,0 +1,339 @@ +// yt - A fully featured command line YouTube client +// +// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of Yt. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +//! These functions interact with the storage db in a read-only way. They are added on-demaned (as +//! you could theoretically just could do everything with the `get_videos` function), as +//! performance or convince requires. +use std::{fs::File, path::PathBuf}; + +use anyhow::{bail, Context, Result}; +use blake3::Hash; +use log::debug; +use sqlx::{query, QueryBuilder, Row, Sqlite}; +use url::Url; +use yt_dlp::wrapper::info_json::InfoJson; + +use crate::{ + app::App, + storage::{ + subscriptions::Subscription, + video_database::{extractor_hash::ExtractorHash, Video}, + }, +}; + +use super::{MpvOptions, VideoOptions, VideoStatus, YtDlpOptions}; + +macro_rules! video_from_record { + ($record:expr) => { + let thumbnail_url = if let Some(url) = &$record.thumbnail_url { + Some(Url::parse(&url)?) + } else { + None + }; + + Ok(Video { + cache_path: $record.cache_path.as_ref().map(|val| PathBuf::from(val)), + description: $record.description.clone(), + duration: $record.duration, + extractor_hash: ExtractorHash::from_hash( + $record + .extractor_hash + .parse() + .expect("The db hash should be a valid blake3 hash"), + ), + last_status_change: $record.last_status_change, + parent_subscription_name: $record.parent_subscription_name.clone(), + publish_date: $record.publish_date, + status: VideoStatus::from_db_integer($record.status), + thumbnail_url, + title: $record.title.clone(), + url: Url::parse(&$record.url)?, + priority: $record.priority, + status_change: if $record.status_change == 1 { + true + } else { + assert_eq!($record.status_change, 0); + false + }, + }) + }; +} + +/// Get the lines to display at the selection file +/// [`changing` = true]: Means that we include *only* videos, that have the `status_changing` flag set +/// [`changing` = None]: Means that we include *both* videos, that have the `status_changing` flag set and not set +pub async fn get_videos( + app: &App, + allowed_states: &[VideoStatus], + changing: Option<bool>, +) -> Result<Vec<Video>> { + let mut qb: QueryBuilder<Sqlite> = QueryBuilder::new( + "\ + SELECT * + FROM videos + WHERE status IN ", + ); + + qb.push("("); + allowed_states + .iter() + .enumerate() + .for_each(|(index, state)| { + qb.push("'"); + qb.push(state.as_db_integer()); + qb.push("'"); + + if index != allowed_states.len() - 1 { + qb.push(","); + } + }); + qb.push(")"); + + if let Some(val) = changing { + if val { + qb.push(" AND status_change = 1"); + } else { + qb.push(" AND status_change = 0"); + } + } + + qb.push("\n ORDER BY priority DESC;"); + + 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> { + let thumbnail_url = if let Some(url) = base.get("thumbnail_url") { + Some(Url::parse(url)?) + } else { + None + }; + Ok(Video { + cache_path: base + .get::<Option<String>, &str>("cache_path") + .as_ref() + .map(|val| PathBuf::from(val)), + description: base.get::<Option<String>, &str>("description").clone(), + duration: base.get("duration"), + extractor_hash: ExtractorHash::from_hash( + base.get::<String, &str>("extractor_hash") + .parse() + .expect("The db hash should be a valid blake3 hash"), + ), + last_status_change: base.get("last_status_change"), + parent_subscription_name: base + .get::<Option<String>, &str>("parent_subscription_name") + .clone(), + publish_date: base.get("publish_date"), + status: VideoStatus::from_db_integer(base.get("status")), + thumbnail_url, + title: base.get::<String, &str>("title").to_owned(), + url: Url::parse(base.get("url"))?, + priority: base.get("priority"), + status_change: { + let val = base.get::<i64, &str>("status_change"); + if val == 1 { + true + } else { + assert_eq!(val, 0, "Can only be 1 or 0"); + false + } + }, + }) + }) + .collect::<Result<Vec<Video>>>()?; + + Ok(real_videos) +} + +pub async fn get_video_info_json(video: &Video) -> Result<Option<InfoJson>> { + if let Some(mut path) = video.cache_path.clone() { + if !path.set_extension("info.json") { + bail!( + "Failed to change path extension to 'info.json': {}", + path.display() + ); + } + let info_json_string = File::open(path)?; + let info_json: InfoJson = serde_json::from_reader(&info_json_string)?; + + Ok(Some(info_json)) + } else { + Ok(None) + } +} + +pub async fn get_video_by_hash(app: &App, hash: &ExtractorHash) -> Result<Video> { + let ehash = hash.hash().to_string(); + + let raw_video = query!( + " + SELECT * FROM videos WHERE extractor_hash = ?; + ", + ehash + ) + .fetch_one(&app.database) + .await?; + + video_from_record! {raw_video} +} + +pub async fn get_currently_playing_video(app: &App) -> Result<Option<Video>> { + let mut videos: Vec<Video> = get_changing_videos(app, VideoStatus::Cached).await?; + + if videos.is_empty() { + Ok(None) + } else { + assert_eq!( + videos.len(), + 1, + "Only one video can change from cached to watched at once!" + ); + + Ok(Some(videos.remove(0))) + } +} + +pub async fn get_changing_videos(app: &App, old_state: VideoStatus) -> Result<Vec<Video>> { + let status = old_state.as_db_integer(); + + let matching = query!( + r#" + SELECT * + FROM videos + WHERE status_change = 1 AND status = ?; + "#, + status + ) + .fetch_all(&app.database) + .await?; + + let real_videos: Vec<Video> = matching + .iter() + .map(|base| -> Result<Video> { + video_from_record! {base} + }) + .collect::<Result<Vec<Video>>>()?; + + Ok(real_videos) +} + +pub async fn get_all_hashes(app: &App) -> Result<Vec<Hash>> { + let hashes_hex = query!( + r#" + SELECT extractor_hash + FROM videos; + "# + ) + .fetch_all(&app.database) + .await?; + + Ok(hashes_hex + .iter() + .map(|hash| { + Hash::from_hex(&hash.extractor_hash) + .expect("These values started as blake3 hashes, they should stay blake3 hashes") + }) + .collect()) +} + +pub async fn get_video_hashes(app: &App, subs: &Subscription) -> Result<Vec<Hash>> { + let hashes_hex = query!( + r#" + SELECT extractor_hash + FROM videos + WHERE parent_subscription_name = ?; + "#, + subs.name + ) + .fetch_all(&app.database) + .await?; + + Ok(hashes_hex + .iter() + .map(|hash| { + Hash::from_hex(&hash.extractor_hash) + .expect("These values started as blake3 hashes, they should stay blake3 hashes") + }) + .collect()) +} + +pub async fn get_video_yt_dlp_opts(app: &App, hash: &ExtractorHash) -> Result<YtDlpOptions> { + let ehash = hash.hash().to_string(); + + let yt_dlp_options = query!( + r#" + SELECT subtitle_langs + FROM video_options + WHERE extractor_hash = ?; + "#, + ehash + ) + .fetch_one(&app.database) + .await?; + + 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?; + + 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?; + + let mpv = MpvOptions { + playback_speed: opts.playback_speed, + }; + let yt_dlp = YtDlpOptions { + subtitle_langs: opts.subtitle_langs, + }; + + Ok(VideoOptions { mpv, yt_dlp }) +} |