use std::future::Future;
#[cfg(persistent_cache)]
use std::path::Path;
use std::path::PathBuf;
#[cfg(persistent_cache)]
use crate::cache::config::{CacheConfig, PersistentBackendKind};
use crate::cache::video::{CachedFile, CachedThumbnail, CachedVideo};
use crate::error::Result;
use crate::model::Video;
use crate::model::playlist::Playlist;
use crate::model::selector::FormatPreferences;
#[cfg(feature = "cache-json")]
pub mod json;
#[cfg(feature = "cache-memory")]
pub mod memory;
#[cfg(feature = "cache-redb")]
pub mod redb;
#[cfg(feature = "cache-redis")]
pub mod redis;
pub(crate) const DEFAULT_VIDEO_TTL: u64 = 24 * 60 * 60;
pub(crate) const DEFAULT_PLAYLIST_TTL: u64 = 6 * 60 * 60;
pub(crate) const DEFAULT_FILE_TTL: u64 = 7 * 24 * 60 * 60;
#[cfg(persistent_cache)]
pub(crate) fn url_hash(url: &str) -> String {
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x00000100000001B3;
let mut hash = FNV_OFFSET;
for byte in url.as_bytes() {
hash ^= *byte as u64;
hash = hash.wrapping_mul(FNV_PRIME);
}
format!("{:016x}", hash)
}
#[cfg(persistent_cache)]
pub(crate) async fn copy_to_cache(cache_dir: &Path, relative_path: &str, source_path: &Path) -> Result<PathBuf> {
let dest_path = cache_dir.join(relative_path);
if let Some(parent) = dest_path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
tokio::fs::copy(source_path, &dest_path).await?;
Ok(dest_path)
}
#[cfg(persistent_cache)]
macro_rules! delegate_to_backend {
($self:ident . $method:ident ( $($arg:expr),* )) => {
match $self {
#[cfg(feature = "cache-json")]
Self::Json(b) => b.$method($($arg),*).await,
#[cfg(feature = "cache-redb")]
Self::Redb(b) => b.$method($($arg),*).await,
#[cfg(feature = "cache-redis")]
Self::Redis(b) => b.$method($($arg),*).await,
}
};
}
#[cfg(feature = "cache-json")]
use json::{JsonFileCache, JsonPlaylistCache, JsonVideoCache};
#[cfg(feature = "cache-redb")]
use redb::{RedbFileCache, RedbPlaylistCache, RedbVideoCache};
#[cfg(feature = "cache-redis")]
use redis::{RedisFileCache, RedisPlaylistCache, RedisVideoCache};
pub trait VideoBackend: Send + Sync + std::fmt::Debug {
fn get(&self, url: &str) -> impl Future<Output = Result<Option<Video>>> + Send;
fn put(&self, url: String, video: Video) -> impl Future<Output = Result<()>> + Send;
fn remove(&self, url: &str) -> impl Future<Output = Result<()>> + Send;
fn clean(&self) -> impl Future<Output = Result<()>> + Send;
fn get_by_id(&self, id: &str) -> impl Future<Output = Result<CachedVideo>> + Send;
}
pub trait PlaylistBackend: Send + Sync + std::fmt::Debug {
fn get(&self, url: &str) -> impl Future<Output = Result<Option<Playlist>>> + Send;
fn get_by_id(&self, id: &str) -> impl Future<Output = Result<Option<Playlist>>> + Send;
fn put(&self, url: String, playlist: Playlist) -> impl Future<Output = Result<()>> + Send;
fn invalidate(&self, url: &str) -> impl Future<Output = Result<()>> + Send;
fn clean(&self) -> impl Future<Output = Result<()>> + Send;
fn clear_all(&self) -> impl Future<Output = Result<()>> + Send;
}
pub trait FileBackend: Send + Sync + std::fmt::Debug {
fn get_by_hash(&self, hash: &str) -> impl Future<Output = Result<Option<(CachedFile, PathBuf)>>> + Send;
fn get_by_video_and_format(
&self,
video_id: &str,
format_id: &str,
) -> impl Future<Output = Result<Option<(CachedFile, PathBuf)>>> + Send;
fn get_by_video_and_preferences(
&self,
video_id: &str,
preferences: &FormatPreferences,
) -> impl Future<Output = Result<Option<(CachedFile, PathBuf)>>> + Send;
fn put(&self, file: CachedFile, source_path: &std::path::Path) -> impl Future<Output = Result<PathBuf>> + Send;
fn remove(&self, id: &str) -> impl Future<Output = Result<()>> + Send;
fn clean(&self) -> impl Future<Output = Result<()>> + Send;
fn get_thumbnail_by_video_id(
&self,
video_id: &str,
) -> impl Future<Output = Result<Option<(CachedThumbnail, PathBuf)>>> + Send;
fn put_thumbnail(
&self,
thumbnail: CachedThumbnail,
source_path: &std::path::Path,
) -> impl Future<Output = Result<PathBuf>> + Send;
fn get_subtitle_by_language(
&self,
video_id: &str,
language: &str,
) -> impl Future<Output = Result<Option<(CachedFile, PathBuf)>>> + Send;
}
#[cfg(persistent_cache)]
#[derive(Debug)]
pub enum PersistentVideoBackend {
#[cfg(feature = "cache-json")]
Json(JsonVideoCache),
#[cfg(feature = "cache-redb")]
Redb(RedbVideoCache),
#[cfg(feature = "cache-redis")]
Redis(RedisVideoCache),
}
#[cfg(persistent_cache)]
#[derive(Debug)]
pub enum PersistentPlaylistBackend {
#[cfg(feature = "cache-json")]
Json(JsonPlaylistCache),
#[cfg(feature = "cache-redb")]
Redb(RedbPlaylistCache),
#[cfg(feature = "cache-redis")]
Redis(RedisPlaylistCache),
}
#[cfg(persistent_cache)]
#[derive(Debug)]
pub enum PersistentFileBackend {
#[cfg(feature = "cache-json")]
Json(JsonFileCache),
#[cfg(feature = "cache-redb")]
Redb(RedbFileCache),
#[cfg(feature = "cache-redis")]
Redis(RedisFileCache),
}
#[cfg(persistent_cache)]
impl PersistentVideoBackend {
pub async fn new(config: &CacheConfig, ttl: Option<u64>) -> Result<Self> {
match PersistentBackendKind::resolve(config.persistent_backend)? {
#[cfg(feature = "cache-json")]
PersistentBackendKind::Json => Ok(Self::Json(JsonVideoCache::new(config.cache_dir.clone(), ttl).await?)),
#[cfg(feature = "cache-redb")]
PersistentBackendKind::Redb => Ok(Self::Redb(RedbVideoCache::new(config.cache_dir.clone(), ttl).await?)),
#[cfg(feature = "cache-redis")]
PersistentBackendKind::Redis => {
let url = config.redis_url.as_deref().unwrap_or("redis://127.0.0.1/");
Ok(Self::Redis(RedisVideoCache::new(url, ttl).await?))
}
}
}
}
#[cfg(persistent_cache)]
impl VideoBackend for PersistentVideoBackend {
async fn get(&self, url: &str) -> Result<Option<Video>> {
delegate_to_backend!(self.get(url))
}
async fn put(&self, url: String, video: Video) -> Result<()> {
delegate_to_backend!(self.put(url, video))
}
async fn remove(&self, url: &str) -> Result<()> {
delegate_to_backend!(self.remove(url))
}
async fn clean(&self) -> Result<()> {
delegate_to_backend!(self.clean())
}
async fn get_by_id(&self, id: &str) -> Result<CachedVideo> {
delegate_to_backend!(self.get_by_id(id))
}
}
#[cfg(persistent_cache)]
impl PersistentPlaylistBackend {
pub async fn new(config: &CacheConfig, ttl: Option<u64>) -> Result<Self> {
match PersistentBackendKind::resolve(config.persistent_backend)? {
#[cfg(feature = "cache-json")]
PersistentBackendKind::Json => Ok(Self::Json(JsonPlaylistCache::new(config.cache_dir.clone(), ttl).await?)),
#[cfg(feature = "cache-redb")]
PersistentBackendKind::Redb => Ok(Self::Redb(RedbPlaylistCache::new(config.cache_dir.clone(), ttl).await?)),
#[cfg(feature = "cache-redis")]
PersistentBackendKind::Redis => {
let url = config.redis_url.as_deref().unwrap_or("redis://127.0.0.1/");
Ok(Self::Redis(RedisPlaylistCache::new(url, ttl).await?))
}
}
}
}
#[cfg(persistent_cache)]
impl PlaylistBackend for PersistentPlaylistBackend {
async fn get(&self, url: &str) -> Result<Option<Playlist>> {
delegate_to_backend!(self.get(url))
}
async fn get_by_id(&self, id: &str) -> Result<Option<Playlist>> {
delegate_to_backend!(self.get_by_id(id))
}
async fn put(&self, url: String, playlist: Playlist) -> Result<()> {
delegate_to_backend!(self.put(url, playlist))
}
async fn invalidate(&self, url: &str) -> Result<()> {
delegate_to_backend!(self.invalidate(url))
}
async fn clean(&self) -> Result<()> {
delegate_to_backend!(self.clean())
}
async fn clear_all(&self) -> Result<()> {
delegate_to_backend!(self.clear_all())
}
}
#[cfg(persistent_cache)]
impl PersistentFileBackend {
pub async fn new(config: &CacheConfig, ttl: Option<u64>) -> Result<Self> {
match PersistentBackendKind::resolve(config.persistent_backend)? {
#[cfg(feature = "cache-json")]
PersistentBackendKind::Json => Ok(Self::Json(JsonFileCache::new(config.cache_dir.clone(), ttl).await?)),
#[cfg(feature = "cache-redb")]
PersistentBackendKind::Redb => Ok(Self::Redb(RedbFileCache::new(config.cache_dir.clone(), ttl).await?)),
#[cfg(feature = "cache-redis")]
PersistentBackendKind::Redis => {
let url = config.redis_url.as_deref().unwrap_or("redis://127.0.0.1/");
Ok(Self::Redis(
RedisFileCache::new(url, config.cache_dir.clone(), ttl).await?,
))
}
}
}
}
#[cfg(persistent_cache)]
impl FileBackend for PersistentFileBackend {
async fn get_by_hash(&self, hash: &str) -> Result<Option<(CachedFile, PathBuf)>> {
delegate_to_backend!(self.get_by_hash(hash))
}
async fn get_by_video_and_format(&self, video_id: &str, format_id: &str) -> Result<Option<(CachedFile, PathBuf)>> {
delegate_to_backend!(self.get_by_video_and_format(video_id, format_id))
}
async fn get_by_video_and_preferences(
&self,
video_id: &str,
preferences: &FormatPreferences,
) -> Result<Option<(CachedFile, PathBuf)>> {
delegate_to_backend!(self.get_by_video_and_preferences(video_id, preferences))
}
async fn put(&self, file: CachedFile, source_path: &std::path::Path) -> Result<PathBuf> {
delegate_to_backend!(self.put(file, source_path))
}
async fn remove(&self, id: &str) -> Result<()> {
delegate_to_backend!(self.remove(id))
}
async fn clean(&self) -> Result<()> {
delegate_to_backend!(self.clean())
}
async fn get_thumbnail_by_video_id(&self, video_id: &str) -> Result<Option<(CachedThumbnail, PathBuf)>> {
delegate_to_backend!(self.get_thumbnail_by_video_id(video_id))
}
async fn put_thumbnail(&self, thumbnail: CachedThumbnail, source_path: &std::path::Path) -> Result<PathBuf> {
delegate_to_backend!(self.put_thumbnail(thumbnail, source_path))
}
async fn get_subtitle_by_language(&self, video_id: &str, language: &str) -> Result<Option<(CachedFile, PathBuf)>> {
delegate_to_backend!(self.get_subtitle_by_language(video_id, language))
}
}