use serde::{Deserialize, Serialize};
#[cfg(persistent_cache)]
use crate::cache::backend::PersistentPlaylistBackend;
use crate::cache::backend::PlaylistBackend;
#[cfg(feature = "cache-memory")]
use crate::cache::backend::memory::MokaPlaylistCache;
use crate::cache::config::CacheConfig;
use crate::error::Result;
use crate::model::playlist::Playlist;
use crate::utils::current_timestamp;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CachedPlaylist {
pub id: String,
pub title: String,
pub url: String,
pub playlist_json: String,
pub cached_at: i64,
}
impl CachedPlaylist {
pub fn playlist(&self) -> Result<Playlist> {
Ok(serde_json::from_str(&self.playlist_json)?)
}
}
impl From<(String, Playlist)> for CachedPlaylist {
fn from((url, playlist): (String, Playlist)) -> Self {
let playlist_json = serde_json::to_string(&playlist).expect("Playlist serialization must not fail");
Self {
id: playlist.id.clone(),
title: playlist.title.clone(),
url,
playlist_json,
cached_at: current_timestamp(),
}
}
}
impl std::fmt::Display for CachedPlaylist {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "CachedPlaylist(id={}, title={})", self.id, self.title)
}
}
#[derive(Debug)]
pub struct PlaylistCache {
#[cfg(feature = "cache-memory")]
memory: MokaPlaylistCache,
#[cfg(persistent_cache)]
persistent: PersistentPlaylistBackend,
}
impl PlaylistCache {
pub async fn new(config: &CacheConfig, ttl: Option<u64>) -> Result<Self> {
tracing::debug!(cache_dir = ?config.cache_dir, ttl = ?ttl, "⚙️ Creating playlist cache");
Ok(Self {
#[cfg(feature = "cache-memory")]
memory: MokaPlaylistCache::new(config.cache_dir.clone(), ttl).await?,
#[cfg(persistent_cache)]
persistent: PersistentPlaylistBackend::new(config, ttl).await?,
})
}
pub async fn get(&self, url: &str) -> Result<Option<Playlist>> {
tracing::debug!(url = url, "🔍 Looking up playlist by URL");
#[cfg(feature = "cache-memory")]
if let Some(playlist) = self.memory.get(url).await? {
tracing::debug!(url = url, "✅ Playlist cache hit (L1 memory)");
return Ok(Some(playlist));
}
#[cfg(persistent_cache)]
if let Some(playlist) = self.persistent.get(url).await? {
tracing::debug!(url = url, "✅ Playlist cache hit (L2 persistent)");
#[cfg(feature = "cache-memory")]
let _ = self.memory.put(url.to_string(), playlist.clone()).await;
return Ok(Some(playlist));
}
Ok(None)
}
pub async fn get_by_id(&self, id: &str) -> Result<Option<Playlist>> {
tracing::debug!(playlist_id = id, "🔍 Looking up playlist by ID");
#[cfg(feature = "cache-memory")]
if let Some(playlist) = self.memory.get_by_id(id).await? {
tracing::debug!(playlist_id = id, "✅ Playlist cache hit by ID (L1 memory)");
return Ok(Some(playlist));
}
#[cfg(persistent_cache)]
if let Some(playlist) = self.persistent.get_by_id(id).await? {
tracing::debug!(playlist_id = id, "✅ Playlist cache hit by ID (L2 persistent)");
return Ok(Some(playlist));
}
Ok(None)
}
pub async fn put(&self, url: String, playlist: Playlist) -> Result<()> {
tracing::debug!(url = url, playlist_id = playlist.id, "⚙️ Storing playlist in cache");
#[cfg(feature = "cache-memory")]
self.memory.put(url.clone(), playlist.clone()).await?;
#[cfg(persistent_cache)]
self.persistent.put(url, playlist).await?;
Ok(())
}
pub async fn invalidate(&self, url: &str) -> Result<()> {
tracing::debug!(url = url, "⚙️ Invalidating playlist in cache");
#[cfg(feature = "cache-memory")]
self.memory.invalidate(url).await?;
#[cfg(persistent_cache)]
self.persistent.invalidate(url).await?;
Ok(())
}
pub async fn clean(&self) -> Result<()> {
tracing::debug!("⚙️ Cleaning playlist cache");
#[cfg(feature = "cache-memory")]
self.memory.clean().await?;
#[cfg(persistent_cache)]
self.persistent.clean().await?;
Ok(())
}
pub async fn clear_all(&self) -> Result<()> {
tracing::debug!("⚙️ Clearing all playlists from cache");
#[cfg(feature = "cache-memory")]
self.memory.clear_all().await?;
#[cfg(persistent_cache)]
self.persistent.clear_all().await?;
Ok(())
}
}