Skip to main content

yt_dlp/cache/stores/
video.rs

1//! Video cache data types and tiered wrapper.
2//!
3//! Provides `CachedVideo`, `CachedFile`, `CachedThumbnail` data structures and the
4//! `VideoCache` wrapper that orchestrates L1 (Moka) and L2 (persistent) lookups.
5
6use 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/// Structure for storing video metadata in cache.
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub struct CachedVideo {
22    /// The ID of the video.
23    pub id: String,
24    /// The title of the video.
25    pub title: String,
26    /// The URL of the video.
27    pub url: String,
28    /// The complete video metadata as JSON.
29    pub video_json: String,
30    /// The cache timestamp (Unix timestamp).
31    pub cached_at: i64,
32}
33
34impl CachedVideo {
35    /// Creates a new `CachedVideo` by serializing the given video.
36    ///
37    /// # Arguments
38    ///
39    /// * `url` - The original URL of the video.
40    /// * `video` - The video metadata to cache.
41    ///
42    /// # Errors
43    ///
44    /// Returns an error if JSON serialization fails.
45    ///
46    /// # Returns
47    ///
48    /// A fully initialized `CachedVideo`.
49    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    /// Deserializes the cached video JSON into a Video struct.
61    ///
62    /// # Returns
63    ///
64    /// The deserialized `Video` object.
65    ///
66    /// # Errors
67    ///
68    /// Returns an error if JSON deserialization fails.
69    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/// Structure for storing downloaded file metadata in cache.
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
82pub struct CachedFile {
83    /// The ID of the file (SHA-256 hash of the content).
84    pub id: String,
85    /// The original filename.
86    pub filename: String,
87    /// The path to the file relative to the cache directory.
88    pub relative_path: String,
89    /// The video ID this file is associated with (if any).
90    pub video_id: Option<String>,
91    /// The file type (format, thumbnail, etc.)
92    pub file_type: String,
93    /// The format ID this file is associated with (if any).
94    pub format_id: Option<String>,
95    /// The format information serialized as JSON (if available).
96    pub format_json: Option<String>,
97    /// The video quality preference used to select this format (if any).
98    pub video_quality: Option<String>,
99    /// The audio quality preference used to select this format (if any).
100    pub audio_quality: Option<String>,
101    /// The video codec preference used to select this format (if any).
102    pub video_codec: Option<String>,
103    /// The audio codec preference used to select this format (if any).
104    pub audio_codec: Option<String>,
105    /// The language code for subtitle files (if any).
106    pub language_code: Option<String>,
107    /// The file size in bytes.
108    pub filesize: i64,
109    /// The MIME type of the file.
110    pub mime_type: String,
111    /// The cache timestamp (Unix timestamp).
112    pub cached_at: i64,
113}
114
115impl CachedFile {
116    /// Checks if this cached file matches the given preferences.
117    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/// Enum representing the type of cached file
157#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
158pub enum CachedType {
159    /// A video or audio format
160    Format,
161    /// A thumbnail image
162    Thumbnail,
163    /// A subtitle file
164    Subtitle,
165    /// Any other type of file
166    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/// Structure for storing thumbnail metadata in cache.
181#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
182pub struct CachedThumbnail {
183    /// The ID of the thumbnail (SHA-256 hash of the content).
184    pub id: String,
185    /// The original filename.
186    pub filename: String,
187    /// The path to the file relative to the cache directory.
188    pub relative_path: String,
189    /// The video ID this thumbnail is associated with.
190    pub video_id: String,
191    /// The file size in bytes.
192    pub filesize: i64,
193    /// The MIME type of the file.
194    pub mime_type: String,
195    /// The width of the thumbnail in pixels (if available).
196    pub width: Option<i32>,
197    /// The height of the thumbnail in pixels (if available).
198    pub height: Option<i32>,
199    /// The cache timestamp (Unix timestamp).
200    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/// Video cache manager with tiered L1 (Moka) + L2 (persistent) lookup.
210///
211/// On `get`: L1 → miss → L2 → backfill L1.
212/// On `put`: write to both layers.
213#[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    /// Creates a new video cache with the configured layers.
223    ///
224    /// # Arguments
225    ///
226    /// * `config` - The cache configuration specifying directories, TTLs, and backend settings.
227    /// * `ttl` - Time-to-live for cache entries in seconds (optional).
228    ///
229    /// # Returns
230    ///
231    /// A new `VideoCache` instance.
232    ///
233    /// # Errors
234    ///
235    /// Returns an error if backend initialization fails or the backend is ambiguous.
236    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    /// Retrieves a video from the cache by its URL.
248    ///
249    /// # Arguments
250    ///
251    /// * `url` - The URL of the video to retrieve.
252    ///
253    /// # Returns
254    ///
255    /// `Some(Video)` if found and not expired, `None` otherwise.
256    ///
257    /// # Errors
258    ///
259    /// Returns an error if the backend query fails.
260    pub async fn get(&self, url: &str) -> Result<Option<Video>> {
261        tracing::debug!(url = url, "🔍 Looking up video by URL");
262
263        // L1: Moka
264        #[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        // L2: persistent
271        #[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            // Backfill L1
276            #[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    /// Puts a video in the cache (both layers).
286    ///
287    /// # Arguments
288    ///
289    /// * `url` - The URL of the video.
290    /// * `video` - The video metadata to cache.
291    ///
292    /// # Errors
293    ///
294    /// Returns an error if the backend put operation fails.
295    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    /// Removes a video from the cache (both layers).
308    ///
309    /// # Arguments
310    ///
311    /// * `url` - The URL of the video to remove.
312    ///
313    /// # Errors
314    ///
315    /// Returns an error if the backend remove operation fails.
316    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    /// Cleans the cache by removing expired entries.
329    ///
330    /// # Errors
331    ///
332    /// Returns an error if the backend clean operation fails.
333    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    /// Retrieves a video from the cache by its ID.
346    ///
347    /// # Arguments
348    ///
349    /// * `id` - The video ID to search for.
350    ///
351    /// # Returns
352    ///
353    /// The cached video metadata.
354    ///
355    /// # Errors
356    ///
357    /// Returns an error if the video is not found or the backend query fails.
358    pub async fn get_by_id(&self, id: &str) -> Result<CachedVideo> {
359        tracing::debug!(video_id = id, "🔍 Looking up video by ID");
360
361        // L1: Moka
362        #[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        // L2: persistent
369        #[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            // Backfill L1
375            #[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}