1use serde::{Deserialize, Serialize};
7
8use crate::cache::FormatPreferences;
9#[cfg(persistent_cache)]
10use crate::cache::backend::PersistentVideoBackend;
11use crate::cache::backend::VideoBackend;
12#[cfg(feature = "cache-memory")]
13use crate::cache::backend::memory::MokaVideoCache;
14use crate::cache::config::CacheConfig;
15use crate::error::Result;
16use crate::model::{Video, utils};
17use crate::utils::current_timestamp;
18
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub struct CachedVideo {
22 pub id: String,
24 pub title: String,
26 pub url: String,
28 pub video_json: String,
30 pub cached_at: i64,
32}
33
34impl CachedVideo {
35 pub fn new(url: String, video: &Video) -> Result<Self> {
50 let video_json = serde_json::to_string(video)?;
51 Ok(Self {
52 id: video.id.clone(),
53 title: video.title.clone(),
54 url,
55 video_json,
56 cached_at: current_timestamp(),
57 })
58 }
59
60 pub fn video(&self) -> Result<Video> {
70 Ok(serde_json::from_str(&self.video_json)?)
71 }
72}
73
74impl std::fmt::Display for CachedVideo {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 write!(f, "CachedVideo(id={}, title={})", self.id, self.title)
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
82pub struct CachedFile {
83 pub id: String,
85 pub filename: String,
87 pub relative_path: String,
89 pub video_id: Option<String>,
91 pub file_type: String,
93 pub format_id: Option<String>,
95 pub format_json: Option<String>,
97 pub video_quality: Option<String>,
99 pub audio_quality: Option<String>,
101 pub video_codec: Option<String>,
103 pub audio_codec: Option<String>,
105 pub language_code: Option<String>,
107 pub filesize: i64,
109 pub mime_type: String,
111 pub cached_at: i64,
113}
114
115impl CachedFile {
116 pub fn matches_preferences(&self, preferences: &FormatPreferences) -> bool {
118 if preferences.video_quality.is_some()
119 && self.video_quality != utils::serde::serialize_json_opt(preferences.video_quality)
120 {
121 return false;
122 }
123
124 if preferences.audio_quality.is_some()
125 && self.audio_quality != utils::serde::serialize_json_opt(preferences.audio_quality)
126 {
127 return false;
128 }
129
130 if preferences.video_codec.is_some()
131 && self.video_codec != utils::serde::serialize_json_opt(preferences.video_codec.clone())
132 {
133 return false;
134 }
135
136 if preferences.audio_codec.is_some()
137 && self.audio_codec != utils::serde::serialize_json_opt(preferences.audio_codec.clone())
138 {
139 return false;
140 }
141
142 true
143 }
144}
145
146impl std::fmt::Display for CachedFile {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 write!(
149 f,
150 "CachedFile(id={}, filename={}, size={})",
151 self.id, self.filename, self.filesize
152 )
153 }
154}
155
156#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
158pub enum CachedType {
159 Format,
161 Thumbnail,
163 Subtitle,
165 Other,
167}
168
169impl std::fmt::Display for CachedType {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 match self {
172 Self::Format => f.write_str("Format"),
173 Self::Thumbnail => f.write_str("Thumbnail"),
174 Self::Subtitle => f.write_str("Subtitle"),
175 Self::Other => f.write_str("Other"),
176 }
177 }
178}
179
180#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
182pub struct CachedThumbnail {
183 pub id: String,
185 pub filename: String,
187 pub relative_path: String,
189 pub video_id: String,
191 pub filesize: i64,
193 pub mime_type: String,
195 pub width: Option<i32>,
197 pub height: Option<i32>,
199 pub cached_at: i64,
201}
202
203impl std::fmt::Display for CachedThumbnail {
204 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 write!(f, "CachedThumbnail(id={}, video_id={})", self.id, self.video_id)
206 }
207}
208
209#[derive(Debug)]
214pub struct VideoCache {
215 #[cfg(feature = "cache-memory")]
216 memory: MokaVideoCache,
217 #[cfg(persistent_cache)]
218 persistent: PersistentVideoBackend,
219}
220
221impl VideoCache {
222 pub async fn new(config: &CacheConfig, ttl: Option<u64>) -> Result<Self> {
237 tracing::debug!(cache_dir = ?config.cache_dir, ttl = ?ttl, "⚙️ Creating video cache");
238
239 Ok(Self {
240 #[cfg(feature = "cache-memory")]
241 memory: MokaVideoCache::new(config.cache_dir.clone(), ttl).await?,
242 #[cfg(persistent_cache)]
243 persistent: PersistentVideoBackend::new(config, ttl).await?,
244 })
245 }
246
247 pub async fn get(&self, url: &str) -> Result<Option<Video>> {
261 tracing::debug!(url = url, "🔍 Looking up video by URL");
262
263 #[cfg(feature = "cache-memory")]
265 if let Some(video) = self.memory.get(url).await? {
266 tracing::debug!(url = url, "✅ Video cache hit (L1 memory)");
267 return Ok(Some(video));
268 }
269
270 #[cfg(persistent_cache)]
272 if let Some(video) = self.persistent.get(url).await? {
273 tracing::debug!(url = url, "✅ Video cache hit (L2 persistent)");
274
275 #[cfg(feature = "cache-memory")]
277 let _ = self.memory.put(url.to_string(), video.clone()).await;
278
279 return Ok(Some(video));
280 }
281
282 Ok(None)
283 }
284
285 pub async fn put(&self, url: String, video: Video) -> Result<()> {
296 tracing::debug!(url = url, video_id = video.id, "⚙️ Storing video in cache");
297
298 #[cfg(feature = "cache-memory")]
299 self.memory.put(url.clone(), video.clone()).await?;
300
301 #[cfg(persistent_cache)]
302 self.persistent.put(url, video).await?;
303
304 Ok(())
305 }
306
307 pub async fn remove(&self, url: &str) -> Result<()> {
317 tracing::debug!(url = url, "⚙️ Removing video from cache");
318
319 #[cfg(feature = "cache-memory")]
320 self.memory.remove(url).await?;
321
322 #[cfg(persistent_cache)]
323 self.persistent.remove(url).await?;
324
325 Ok(())
326 }
327
328 pub async fn clean(&self) -> Result<()> {
334 tracing::debug!("⚙️ Cleaning video cache");
335
336 #[cfg(feature = "cache-memory")]
337 self.memory.clean().await?;
338
339 #[cfg(persistent_cache)]
340 self.persistent.clean().await?;
341
342 Ok(())
343 }
344
345 pub async fn get_by_id(&self, id: &str) -> Result<CachedVideo> {
359 tracing::debug!(video_id = id, "🔍 Looking up video by ID");
360
361 #[cfg(feature = "cache-memory")]
363 if let Ok(cached) = self.memory.get_by_id(id).await {
364 tracing::debug!(video_id = id, "✅ Video cache hit by ID (L1 memory)");
365 return Ok(cached);
366 }
367
368 #[cfg(persistent_cache)]
370 let result = {
371 let cached = self.persistent.get_by_id(id).await?;
372 tracing::debug!(video_id = id, "✅ Video cache hit by ID (L2 persistent)");
373
374 #[cfg(feature = "cache-memory")]
376 if let Ok(video) = cached.video() {
377 let _ = self.memory.put(cached.url.clone(), video).await;
378 }
379
380 Ok(cached)
381 };
382 #[cfg(not(persistent_cache))]
383 let result = Err(crate::error::Error::cache_miss(format!("video:{}", id)));
384
385 result
386 }
387}