about summary refs log blame commit diff stats
path: root/src/storage/video_database/downloader.rs
blob: c04ab8d732df38c6e3c7dafb055ff5e97a486d45 (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>.

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};

use super::{ExtractorHash, Video};

/// Returns to next video which should be downloaded. This respects the priority assigned by select.
/// It does not return videos, which are already cached.
pub async fn get_next_uncached_video(app: &App) -> Result<Option<Video>> {
    let status = VideoStatus::Watch.as_db_integer();

    let result = query!(
        r#"
        SELECT *
        FROM videos
        WHERE status = ? AND cache_path IS NULL
        ORDER BY priority ASC
        LIMIT 1;
    "#,
        status
    )
    .fetch_one(&app.database)
    .await;

    if let Err(sqlx::Error::RowNotFound) = result {
        Ok(None)
    } else {
        let base = result?;

        let thumbnail_url = if let Some(url) = &base.thumbnail_url {
            Some(Url::parse(&url)?)
        } else {
            None
        };

        let status_change = if base.status_change == 1 {
            true
        } else {
            assert_eq!(base.status_change, 0, "Can only be 1 or 0");
            false
        };

        let video = Video {
            cache_path: base.cache_path.as_ref().map(|val| PathBuf::from(val)),
            description: base.description.clone(),
            duration: base.duration,
            extractor_hash: ExtractorHash::from_hash(
                base.extractor_hash
                    .parse()
                    .expect("The hash in the db should be valid"),
            ),
            last_status_change: base.last_status_change,
            parent_subscription_name: base.parent_subscription_name.clone(),
            priority: base.priority,
            publish_date: base.publish_date,
            status: VideoStatus::from_db_integer(base.status),
            status_change,
            thumbnail_url,
            title: base.title.clone(),
            url: Url::parse(&base.url)?,
        };

        Ok(Some(video))
    }
}

/// Returns to next video which can be watched (i.e. is cached).
/// This respects the priority assigned by select.
pub async fn get_next_video_watchable(app: &App) -> Result<Option<Video>> {
    let result = query!(
        r#"
        SELECT *
        FROM videos
        WHERE status = 'Watching' AND cache_path IS NOT NULL
        ORDER BY priority ASC
        LIMIT 1;
    "#
    )
    .fetch_one(&app.database)
    .await;

    if let Err(sqlx::Error::RowNotFound) = result {
        Ok(None)
    } else {
        let base = result?;

        let thumbnail_url = if let Some(url) = &base.thumbnail_url {
            Some(Url::parse(&url)?)
        } else {
            None
        };

        let status_change = if base.status_change == 1 {
            true
        } else {
            assert_eq!(base.status_change, 0, "Can only be 1 or 0");
            false
        };

        let video = Video {
            cache_path: base.cache_path.as_ref().map(|val| PathBuf::from(val)),
            description: base.description.clone(),
            duration: base.duration,
            extractor_hash: ExtractorHash::from_hash(
                base.extractor_hash
                    .parse()
                    .expect("The db extractor_hash should be valid blake3 hash"),
            ),
            last_status_change: base.last_status_change,
            parent_subscription_name: base.parent_subscription_name.clone(),
            priority: base.priority,
            publish_date: base.publish_date,
            status: VideoStatus::from_db_integer(base.status),
            status_change,
            thumbnail_url,
            title: base.title.clone(),
            url: Url::parse(&base.url)?,
        };

        Ok(Some(video))
    }
}

/// Update the cached path of a video. Will be set to NULL if the path is None
/// This will also set the status to `Cached` when path is Some, otherwise it set's the status to
/// `Watch`.
pub async fn set_video_cache_path(
    app: &App,
    video: &ExtractorHash,
    path: Option<&Path>,
) -> Result<()> {
    if let Some(path) = path {
        debug!(
            "Setting cache path from '{}' to '{}'",
            video.into_short_hash(app).await?,
            path.display()
        );

        let path_str = path.display().to_string();
        let extractor_hash = video.hash().to_string();
        let status = VideoStatus::Cached.as_db_integer();

        query!(
            r#"
            UPDATE videos
            SET cache_path = ?, status = ?
            WHERE extractor_hash = ?;
        "#,
            path_str,
            status,
            extractor_hash
        )
        .execute(&app.database)
        .await?;

        Ok(())
    } else {
        debug!(
            "Setting cache path from '{}' to NULL",
            video.into_short_hash(app).await?,
        );

        let extractor_hash = video.hash().to_string();
        let status = VideoStatus::Watch.as_db_integer();

        query!(
            r#"
            UPDATE videos
            SET cache_path = NULL, status = ?
            WHERE extractor_hash = ?;
        "#,
            status,
            extractor_hash
        )
        .execute(&app.database)
        .await?;

        Ok(())
    }
}

/// Returns the number of cached videos
pub async fn get_allocated_cache(app: &App) -> Result<u32> {
    let count = query!(
        r#"
        SELECT COUNT(cache_path) as count
        FROM videos
        WHERE cache_path IS NOT NULL;
"#,
    )
    .fetch_one(&app.database)
    .await?;

    Ok(count.count as u32)
}