aboutsummaryrefslogtreecommitdiffstats
path: root/src/storage/video_database/getters.rs
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-08-21 10:49:23 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-08-21 11:28:43 +0200
commit1debeb77f7986de1b659dcfdc442de6415e1d9f5 (patch)
tree4df3e7c3f6a2d1ec116e4088c5ace7f143a8b05f /src/storage/video_database/getters.rs
downloadyt-1debeb77f7986de1b659dcfdc442de6415e1d9f5.zip
chore: Initial Commit
This repository was migrated out of my nixos-config.
Diffstat (limited to '')
-rw-r--r--src/storage/video_database/getters.rs339
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 })
+}