about summary refs log tree commit diff stats
path: root/src/storage/video_database/getters.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/storage/video_database/getters.rs')
-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 })
+}