use serde::{Deserialize, Serialize};
use crate::cache::FormatPreferences;
#[cfg(persistent_cache)]
use crate::cache::backend::PersistentVideoBackend;
use crate::cache::backend::VideoBackend;
#[cfg(feature = "cache-memory")]
use crate::cache::backend::memory::MokaVideoCache;
use crate::cache::config::CacheConfig;
use crate::error::Result;
use crate::model::{Video, utils};
use crate::utils::current_timestamp;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CachedVideo {
pub id: String,
pub title: String,
pub url: String,
pub video_json: String,
pub cached_at: i64,
}
impl CachedVideo {
pub fn new(url: String, video: &Video) -> Result<Self> {
let video_json = serde_json::to_string(video)?;
Ok(Self {
id: video.id.clone(),
title: video.title.clone(),
url,
video_json,
cached_at: current_timestamp(),
})
}
pub fn video(&self) -> Result<Video> {
Ok(serde_json::from_str(&self.video_json)?)
}
}
impl std::fmt::Display for CachedVideo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "CachedVideo(id={}, title={})", self.id, self.title)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CachedFile {
pub id: String,
pub filename: String,
pub relative_path: String,
pub video_id: Option<String>,
pub file_type: String,
pub format_id: Option<String>,
pub format_json: Option<String>,
pub video_quality: Option<String>,
pub audio_quality: Option<String>,
pub video_codec: Option<String>,
pub audio_codec: Option<String>,
pub language_code: Option<String>,
pub filesize: i64,
pub mime_type: String,
pub cached_at: i64,
}
impl CachedFile {
pub fn matches_preferences(&self, preferences: &FormatPreferences) -> bool {
if preferences.video_quality.is_some()
&& self.video_quality != utils::serde::serialize_json_opt(preferences.video_quality)
{
return false;
}
if preferences.audio_quality.is_some()
&& self.audio_quality != utils::serde::serialize_json_opt(preferences.audio_quality)
{
return false;
}
if preferences.video_codec.is_some()
&& self.video_codec != utils::serde::serialize_json_opt(preferences.video_codec.clone())
{
return false;
}
if preferences.audio_codec.is_some()
&& self.audio_codec != utils::serde::serialize_json_opt(preferences.audio_codec.clone())
{
return false;
}
true
}
}
impl std::fmt::Display for CachedFile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"CachedFile(id={}, filename={}, size={})",
self.id, self.filename, self.filesize
)
}
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub enum CachedType {
Format,
Thumbnail,
Subtitle,
Other,
}
impl std::fmt::Display for CachedType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Format => f.write_str("Format"),
Self::Thumbnail => f.write_str("Thumbnail"),
Self::Subtitle => f.write_str("Subtitle"),
Self::Other => f.write_str("Other"),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CachedThumbnail {
pub id: String,
pub filename: String,
pub relative_path: String,
pub video_id: String,
pub filesize: i64,
pub mime_type: String,
pub width: Option<i32>,
pub height: Option<i32>,
pub cached_at: i64,
}
impl std::fmt::Display for CachedThumbnail {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "CachedThumbnail(id={}, video_id={})", self.id, self.video_id)
}
}
#[derive(Debug)]
pub struct VideoCache {
#[cfg(feature = "cache-memory")]
memory: MokaVideoCache,
#[cfg(persistent_cache)]
persistent: PersistentVideoBackend,
}
impl VideoCache {
pub async fn new(config: &CacheConfig, ttl: Option<u64>) -> Result<Self> {
tracing::debug!(cache_dir = ?config.cache_dir, ttl = ?ttl, "⚙️ Creating video cache");
Ok(Self {
#[cfg(feature = "cache-memory")]
memory: MokaVideoCache::new(config.cache_dir.clone(), ttl).await?,
#[cfg(persistent_cache)]
persistent: PersistentVideoBackend::new(config, ttl).await?,
})
}
pub async fn get(&self, url: &str) -> Result<Option<Video>> {
tracing::debug!(url = url, "🔍 Looking up video by URL");
#[cfg(feature = "cache-memory")]
if let Some(video) = self.memory.get(url).await? {
tracing::debug!(url = url, "✅ Video cache hit (L1 memory)");
return Ok(Some(video));
}
#[cfg(persistent_cache)]
if let Some(video) = self.persistent.get(url).await? {
tracing::debug!(url = url, "✅ Video cache hit (L2 persistent)");
#[cfg(feature = "cache-memory")]
let _ = self.memory.put(url.to_string(), video.clone()).await;
return Ok(Some(video));
}
Ok(None)
}
pub async fn put(&self, url: String, video: Video) -> Result<()> {
tracing::debug!(url = url, video_id = video.id, "⚙️ Storing video in cache");
#[cfg(feature = "cache-memory")]
self.memory.put(url.clone(), video.clone()).await?;
#[cfg(persistent_cache)]
self.persistent.put(url, video).await?;
Ok(())
}
pub async fn remove(&self, url: &str) -> Result<()> {
tracing::debug!(url = url, "⚙️ Removing video from cache");
#[cfg(feature = "cache-memory")]
self.memory.remove(url).await?;
#[cfg(persistent_cache)]
self.persistent.remove(url).await?;
Ok(())
}
pub async fn clean(&self) -> Result<()> {
tracing::debug!("⚙️ Cleaning video cache");
#[cfg(feature = "cache-memory")]
self.memory.clean().await?;
#[cfg(persistent_cache)]
self.persistent.clean().await?;
Ok(())
}
pub async fn get_by_id(&self, id: &str) -> Result<CachedVideo> {
tracing::debug!(video_id = id, "🔍 Looking up video by ID");
#[cfg(feature = "cache-memory")]
if let Ok(cached) = self.memory.get_by_id(id).await {
tracing::debug!(video_id = id, "✅ Video cache hit by ID (L1 memory)");
return Ok(cached);
}
#[cfg(persistent_cache)]
let result = {
let cached = self.persistent.get_by_id(id).await?;
tracing::debug!(video_id = id, "✅ Video cache hit by ID (L2 persistent)");
#[cfg(feature = "cache-memory")]
if let Ok(video) = cached.video() {
let _ = self.memory.put(cached.url.clone(), video).await;
}
Ok(cached)
};
#[cfg(not(persistent_cache))]
let result = Err(crate::error::Error::cache_miss(format!("video:{}", id)));
result
}
}