// 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)
}