about summary refs log blame commit diff stats
path: root/src/storage/video_database/getters.rs
blob: 176ebbbb9de3d6ef12bf7dc11306fec7150c096d (plain) (tree)

































                                                                                                  
                                                                                   


















                                                                                  
                                                                                           















































                                                                                                                  
                                                                















                                                                            
















                                                                                     


                                                                                                 
                                                                    
                                                                                                  



























































































































































































                                                                                                
// 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).expect("Parsing this as url should always work"))
        } 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).expect("Parsing this as url should always work"),
            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, 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<Video> = videos
        .iter()
        .map(|base| -> Result<Video> {
            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: base
                    .get::<Option<String>, &str>("thumbnail_url")
                    .as_ref()
                    .map(|url| Url::parse(url).expect("Parsing this as url should always work")),
                title: base.get::<String, &str>("title").to_owned(),
                url: Url::parse(base.get("url")).expect("Parsing this as url should always work"),
                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 })
}