// yt - A fully featured command line YouTube client // // Copyright (C) 2024 Benedikt Peetz // 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 . 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> { 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).expect("Parsing this as url should always work")) } 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).expect("Parsing this as url should always work"), }; 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> { 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).expect("Parsing this as url should always work")) } 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).expect("Parsing this as url should always work"), }; 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 { 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) }