Skip to main content

yt_dlp/cache/backend/
mod.rs

1//! Cache backend trait definitions and dispatch enums.
2//!
3//! This module defines the backend traits (`VideoBackend`, `PlaylistBackend`, `FileBackend`)
4//! and provides persistent-layer dispatch enums that delegate to the correct concrete
5//! backend based on enabled features. The in-memory Moka backend is separate and used
6//! as the L1 layer; the persistent enum is the L2 layer.
7
8use std::future::Future;
9#[cfg(persistent_cache)]
10use std::path::Path;
11use std::path::PathBuf;
12
13#[cfg(persistent_cache)]
14use crate::cache::config::{CacheConfig, PersistentBackendKind};
15use crate::cache::video::{CachedFile, CachedThumbnail, CachedVideo};
16use crate::error::Result;
17use crate::model::Video;
18use crate::model::playlist::Playlist;
19use crate::model::selector::FormatPreferences;
20
21#[cfg(feature = "cache-json")]
22pub mod json;
23#[cfg(feature = "cache-memory")]
24pub mod memory;
25#[cfg(feature = "cache-redb")]
26pub mod redb;
27#[cfg(feature = "cache-redis")]
28pub mod redis;
29
30// ── Shared constants ──
31
32/// Default time-to-live for cached videos (24 hours).
33pub(crate) const DEFAULT_VIDEO_TTL: u64 = 24 * 60 * 60;
34/// Default time-to-live for cached playlists (6 hours).
35pub(crate) const DEFAULT_PLAYLIST_TTL: u64 = 6 * 60 * 60;
36/// Default time-to-live for cached files (7 days).
37pub(crate) const DEFAULT_FILE_TTL: u64 = 7 * 24 * 60 * 60;
38
39// ── Shared helpers ──
40
41/// Compute a stable FNV-1a 64-bit hex hash of a URL.
42///
43/// Uses a manual implementation for cross-version stability
44/// (unlike `DefaultHasher`, which can change between Rust releases).
45#[cfg(persistent_cache)]
46pub(crate) fn url_hash(url: &str) -> String {
47    const FNV_OFFSET: u64 = 0xcbf29ce484222325;
48    const FNV_PRIME: u64 = 0x00000100000001B3;
49    let mut hash = FNV_OFFSET;
50    for byte in url.as_bytes() {
51        hash ^= *byte as u64;
52        hash = hash.wrapping_mul(FNV_PRIME);
53    }
54    format!("{:016x}", hash)
55}
56
57/// Copy a source file into the cache directory, creating parent directories as needed.
58///
59/// Returns the destination path (`cache_dir` joined with `relative_path`).
60#[cfg(persistent_cache)]
61pub(crate) async fn copy_to_cache(cache_dir: &Path, relative_path: &str, source_path: &Path) -> Result<PathBuf> {
62    let dest_path = cache_dir.join(relative_path);
63    if let Some(parent) = dest_path.parent() {
64        tokio::fs::create_dir_all(parent).await?;
65    }
66    tokio::fs::copy(source_path, &dest_path).await?;
67    Ok(dest_path)
68}
69
70/// Delegates a method call to the active backend variant.
71///
72/// Expands to a `match self` block that forwards the call to whichever
73/// concrete backend is selected at runtime, respecting feature gates.
74#[cfg(persistent_cache)]
75macro_rules! delegate_to_backend {
76    ($self:ident . $method:ident ( $($arg:expr),* )) => {
77        match $self {
78            #[cfg(feature = "cache-json")]
79            Self::Json(b) => b.$method($($arg),*).await,
80            #[cfg(feature = "cache-redb")]
81            Self::Redb(b) => b.$method($($arg),*).await,
82            #[cfg(feature = "cache-redis")]
83            Self::Redis(b) => b.$method($($arg),*).await,
84        }
85    };
86}
87
88#[cfg(feature = "cache-json")]
89use json::{JsonFileCache, JsonPlaylistCache, JsonVideoCache};
90#[cfg(feature = "cache-redb")]
91use redb::{RedbFileCache, RedbPlaylistCache, RedbVideoCache};
92#[cfg(feature = "cache-redis")]
93use redis::{RedisFileCache, RedisPlaylistCache, RedisVideoCache};
94
95/// Trait for video cache backend implementations.
96pub trait VideoBackend: Send + Sync + std::fmt::Debug {
97    /// Retrieves a video by its URL.
98    ///
99    /// # Arguments
100    ///
101    /// * `url` - The URL of the video to retrieve
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if the backend lookup fails.
106    ///
107    /// # Returns
108    ///
109    /// The cached `Video` if found, or `None` if not present.
110    fn get(&self, url: &str) -> impl Future<Output = Result<Option<Video>>> + Send;
111
112    /// Stores a video in the cache.
113    ///
114    /// # Arguments
115    ///
116    /// * `url` - The URL to use as the cache key
117    /// * `video` - The video metadata to cache
118    ///
119    /// # Errors
120    ///
121    /// Returns an error if the write operation fails.
122    fn put(&self, url: String, video: Video) -> impl Future<Output = Result<()>> + Send;
123
124    /// Removes a video from the cache by URL.
125    ///
126    /// # Arguments
127    ///
128    /// * `url` - The URL of the video to remove
129    ///
130    /// # Errors
131    ///
132    /// Returns an error if the removal operation fails.
133    fn remove(&self, url: &str) -> impl Future<Output = Result<()>> + Send;
134
135    /// Cleans expired entries from the cache.
136    ///
137    /// # Errors
138    ///
139    /// Returns an error if the cleanup operation fails.
140    fn clean(&self) -> impl Future<Output = Result<()>> + Send;
141
142    /// Retrieves a video by its ID.
143    ///
144    /// # Arguments
145    ///
146    /// * `id` - The unique identifier of the video
147    ///
148    /// # Errors
149    ///
150    /// Returns an error if the backend lookup fails.
151    ///
152    /// # Returns
153    ///
154    /// The cached video entry.
155    fn get_by_id(&self, id: &str) -> impl Future<Output = Result<CachedVideo>> + Send;
156}
157
158/// Trait for playlist cache backend implementations.
159pub trait PlaylistBackend: Send + Sync + std::fmt::Debug {
160    /// Retrieves a playlist by its URL.
161    ///
162    /// # Arguments
163    ///
164    /// * `url` - The URL of the playlist to retrieve
165    ///
166    /// # Errors
167    ///
168    /// Returns an error if the backend lookup fails.
169    ///
170    /// # Returns
171    ///
172    /// The cached `Playlist` if found, or `None` if not present.
173    fn get(&self, url: &str) -> impl Future<Output = Result<Option<Playlist>>> + Send;
174
175    /// Retrieves a playlist by its ID.
176    ///
177    /// # Arguments
178    ///
179    /// * `id` - The unique identifier of the playlist
180    ///
181    /// # Errors
182    ///
183    /// Returns an error if the backend lookup fails.
184    ///
185    /// # Returns
186    ///
187    /// The cached `Playlist` if found, or `None` if not present.
188    fn get_by_id(&self, id: &str) -> impl Future<Output = Result<Option<Playlist>>> + Send;
189
190    /// Stores a playlist in the cache.
191    ///
192    /// # Arguments
193    ///
194    /// * `url` - The URL to use as the cache key
195    /// * `playlist` - The playlist to cache
196    ///
197    /// # Errors
198    ///
199    /// Returns an error if the write operation fails.
200    fn put(&self, url: String, playlist: Playlist) -> impl Future<Output = Result<()>> + Send;
201
202    /// Invalidates (removes) a playlist from the cache by URL.
203    ///
204    /// # Arguments
205    ///
206    /// * `url` - The URL of the playlist to invalidate
207    ///
208    /// # Errors
209    ///
210    /// Returns an error if the invalidation operation fails.
211    fn invalidate(&self, url: &str) -> impl Future<Output = Result<()>> + Send;
212
213    /// Cleans expired entries from the cache.
214    ///
215    /// # Errors
216    ///
217    /// Returns an error if the cleanup operation fails.
218    fn clean(&self) -> impl Future<Output = Result<()>> + Send;
219
220    /// Clears all entries from the cache.
221    ///
222    /// # Errors
223    ///
224    /// Returns an error if the clear operation fails.
225    fn clear_all(&self) -> impl Future<Output = Result<()>> + Send;
226}
227
228/// Trait for file cache backend implementations.
229pub trait FileBackend: Send + Sync + std::fmt::Debug {
230    /// Retrieves a file from the cache by its hash.
231    ///
232    /// # Arguments
233    ///
234    /// * `hash` - The content hash of the file
235    ///
236    /// # Returns
237    ///
238    /// The cached file entry and its path, or `None` if not found.
239    ///
240    /// # Errors
241    ///
242    /// Returns an error if the underlying I/O or deserialization fails.
243    fn get_by_hash(&self, hash: &str) -> impl Future<Output = Result<Option<(CachedFile, PathBuf)>>> + Send;
244
245    /// Retrieves a file from the cache by video ID and format ID.
246    ///
247    /// # Arguments
248    ///
249    /// * `video_id` - The video identifier
250    /// * `format_id` - The format identifier
251    ///
252    /// # Returns
253    ///
254    /// The cached file entry and its path, or `None` if not found.
255    ///
256    /// # Errors
257    ///
258    /// Returns an error if the underlying I/O or deserialization fails.
259    fn get_by_video_and_format(
260        &self,
261        video_id: &str,
262        format_id: &str,
263    ) -> impl Future<Output = Result<Option<(CachedFile, PathBuf)>>> + Send;
264
265    /// Retrieves a file from the cache based on video ID and quality preferences.
266    ///
267    /// # Arguments
268    ///
269    /// * `video_id` - The video identifier
270    /// * `preferences` - The format preferences to match against
271    ///
272    /// # Returns
273    ///
274    /// The cached file entry and its path, or `None` if no match.
275    ///
276    /// # Errors
277    ///
278    /// Returns an error if the underlying I/O or deserialization fails.
279    fn get_by_video_and_preferences(
280        &self,
281        video_id: &str,
282        preferences: &FormatPreferences,
283    ) -> impl Future<Output = Result<Option<(CachedFile, PathBuf)>>> + Send;
284
285    /// Store a file in the cache.
286    ///
287    /// # Arguments
288    ///
289    /// * `file` - The cached file metadata
290    /// * `source_path` - Path to the source file to store
291    ///
292    /// # Errors
293    ///
294    /// Returns an error if the file cannot be stored.
295    ///
296    /// # Returns
297    ///
298    /// The path where the file was cached.
299    fn put(&self, file: CachedFile, source_path: &std::path::Path) -> impl Future<Output = Result<PathBuf>> + Send;
300
301    /// Removes a file from the cache by its ID.
302    ///
303    /// # Arguments
304    ///
305    /// * `id` - The unique identifier of the cached file
306    ///
307    /// # Errors
308    ///
309    /// Returns an error if the removal operation fails.
310    fn remove(&self, id: &str) -> impl Future<Output = Result<()>> + Send;
311
312    /// Cleans expired entries from the cache.
313    ///
314    /// # Errors
315    ///
316    /// Returns an error if the cleanup operation fails.
317    fn clean(&self) -> impl Future<Output = Result<()>> + Send;
318
319    /// Retrieve a thumbnail from the cache by video ID.
320    ///
321    /// # Arguments
322    ///
323    /// * `video_id` - The video identifier
324    ///
325    /// # Returns
326    ///
327    /// The cached thumbnail entry and its path, or `None` if not found.
328    ///
329    /// # Errors
330    ///
331    /// Returns an error if the underlying I/O or deserialization fails.
332    fn get_thumbnail_by_video_id(
333        &self,
334        video_id: &str,
335    ) -> impl Future<Output = Result<Option<(CachedThumbnail, PathBuf)>>> + Send;
336
337    /// Store a thumbnail in the cache.
338    ///
339    /// # Arguments
340    ///
341    /// * `thumbnail` - The cached thumbnail metadata
342    /// * `source_path` - Path to the source thumbnail file
343    ///
344    /// # Errors
345    ///
346    /// Returns an error if the thumbnail cannot be stored.
347    ///
348    /// # Returns
349    ///
350    /// The path where the thumbnail was cached.
351    fn put_thumbnail(
352        &self,
353        thumbnail: CachedThumbnail,
354        source_path: &std::path::Path,
355    ) -> impl Future<Output = Result<PathBuf>> + Send;
356
357    /// Retrieve a subtitle from the cache by video ID and language.
358    ///
359    /// # Arguments
360    ///
361    /// * `video_id` - The video identifier
362    /// * `language` - The subtitle language code
363    ///
364    /// # Returns
365    ///
366    /// The cached subtitle file entry and its path, or `None` if not found.
367    ///
368    /// # Errors
369    ///
370    /// Returns an error if the underlying I/O or deserialization fails.
371    fn get_subtitle_by_language(
372        &self,
373        video_id: &str,
374        language: &str,
375    ) -> impl Future<Output = Result<Option<(CachedFile, PathBuf)>>> + Send;
376}
377
378// ── Persistent backend dispatch enums ──
379
380/// Enum dispatch for persistent video backends.
381///
382/// All features' variants are included when their respective feature is enabled.
383/// The active backend is selected at construction time via `PersistentBackendKind::resolve`.
384#[cfg(persistent_cache)]
385#[derive(Debug)]
386pub enum PersistentVideoBackend {
387    #[cfg(feature = "cache-json")]
388    Json(JsonVideoCache),
389    #[cfg(feature = "cache-redb")]
390    Redb(RedbVideoCache),
391    #[cfg(feature = "cache-redis")]
392    Redis(RedisVideoCache),
393}
394
395/// Enum dispatch for persistent playlist backends.
396#[cfg(persistent_cache)]
397#[derive(Debug)]
398pub enum PersistentPlaylistBackend {
399    #[cfg(feature = "cache-json")]
400    Json(JsonPlaylistCache),
401    #[cfg(feature = "cache-redb")]
402    Redb(RedbPlaylistCache),
403    #[cfg(feature = "cache-redis")]
404    Redis(RedisPlaylistCache),
405}
406
407/// Enum dispatch for persistent file backends.
408#[cfg(persistent_cache)]
409#[derive(Debug)]
410pub enum PersistentFileBackend {
411    #[cfg(feature = "cache-json")]
412    Json(JsonFileCache),
413    #[cfg(feature = "cache-redb")]
414    Redb(RedbFileCache),
415    #[cfg(feature = "cache-redis")]
416    Redis(RedisFileCache),
417}
418
419// ── Persistent video backend constructors & dispatch ──
420
421#[cfg(persistent_cache)]
422impl PersistentVideoBackend {
423    /// Creates the persistent video backend for the selected kind.
424    ///
425    /// # Arguments
426    ///
427    /// * `config` - The cache configuration specifying directories, TTLs, and backend settings.
428    /// * `ttl` - Time-to-live in seconds
429    ///
430    /// # Errors
431    ///
432    /// Returns `Error::AmbiguousCacheBackend` if `kind` is `None` and multiple backends are compiled in.
433    /// Returns an error if the selected backend fails to initialize.
434    pub async fn new(config: &CacheConfig, ttl: Option<u64>) -> Result<Self> {
435        match PersistentBackendKind::resolve(config.persistent_backend)? {
436            #[cfg(feature = "cache-json")]
437            PersistentBackendKind::Json => Ok(Self::Json(JsonVideoCache::new(config.cache_dir.clone(), ttl).await?)),
438            #[cfg(feature = "cache-redb")]
439            PersistentBackendKind::Redb => Ok(Self::Redb(RedbVideoCache::new(config.cache_dir.clone(), ttl).await?)),
440            #[cfg(feature = "cache-redis")]
441            PersistentBackendKind::Redis => {
442                let url = config.redis_url.as_deref().unwrap_or("redis://127.0.0.1/");
443                Ok(Self::Redis(RedisVideoCache::new(url, ttl).await?))
444            }
445        }
446    }
447}
448
449#[cfg(persistent_cache)]
450impl VideoBackend for PersistentVideoBackend {
451    async fn get(&self, url: &str) -> Result<Option<Video>> {
452        delegate_to_backend!(self.get(url))
453    }
454
455    async fn put(&self, url: String, video: Video) -> Result<()> {
456        delegate_to_backend!(self.put(url, video))
457    }
458
459    async fn remove(&self, url: &str) -> Result<()> {
460        delegate_to_backend!(self.remove(url))
461    }
462
463    async fn clean(&self) -> Result<()> {
464        delegate_to_backend!(self.clean())
465    }
466
467    async fn get_by_id(&self, id: &str) -> Result<CachedVideo> {
468        delegate_to_backend!(self.get_by_id(id))
469    }
470}
471
472// ── Persistent playlist backend constructors & dispatch ──
473
474#[cfg(persistent_cache)]
475impl PersistentPlaylistBackend {
476    /// Creates the persistent playlist backend for the selected kind.
477    ///
478    /// # Arguments
479    ///
480    /// * `config` - The cache configuration specifying directories, TTLs, and backend settings.
481    /// * `ttl` - Time-to-live in seconds
482    ///
483    /// # Errors
484    ///
485    /// Returns `Error::AmbiguousCacheBackend` if `kind` is `None` and multiple backends are compiled in.
486    /// Returns an error if the selected backend fails to initialize.
487    pub async fn new(config: &CacheConfig, ttl: Option<u64>) -> Result<Self> {
488        match PersistentBackendKind::resolve(config.persistent_backend)? {
489            #[cfg(feature = "cache-json")]
490            PersistentBackendKind::Json => Ok(Self::Json(JsonPlaylistCache::new(config.cache_dir.clone(), ttl).await?)),
491            #[cfg(feature = "cache-redb")]
492            PersistentBackendKind::Redb => Ok(Self::Redb(RedbPlaylistCache::new(config.cache_dir.clone(), ttl).await?)),
493            #[cfg(feature = "cache-redis")]
494            PersistentBackendKind::Redis => {
495                let url = config.redis_url.as_deref().unwrap_or("redis://127.0.0.1/");
496                Ok(Self::Redis(RedisPlaylistCache::new(url, ttl).await?))
497            }
498        }
499    }
500}
501
502#[cfg(persistent_cache)]
503impl PlaylistBackend for PersistentPlaylistBackend {
504    async fn get(&self, url: &str) -> Result<Option<Playlist>> {
505        delegate_to_backend!(self.get(url))
506    }
507
508    async fn get_by_id(&self, id: &str) -> Result<Option<Playlist>> {
509        delegate_to_backend!(self.get_by_id(id))
510    }
511
512    async fn put(&self, url: String, playlist: Playlist) -> Result<()> {
513        delegate_to_backend!(self.put(url, playlist))
514    }
515
516    async fn invalidate(&self, url: &str) -> Result<()> {
517        delegate_to_backend!(self.invalidate(url))
518    }
519
520    async fn clean(&self) -> Result<()> {
521        delegate_to_backend!(self.clean())
522    }
523
524    async fn clear_all(&self) -> Result<()> {
525        delegate_to_backend!(self.clear_all())
526    }
527}
528
529// ── Persistent file backend constructors & dispatch ──
530
531#[cfg(persistent_cache)]
532impl PersistentFileBackend {
533    /// Creates the persistent file backend for the selected kind.
534    ///
535    /// # Arguments
536    ///
537    /// * `config` - The cache configuration specifying directories, TTLs, and backend settings.
538    /// * `ttl` - Time-to-live in seconds
539    ///
540    /// # Errors
541    ///
542    /// Returns `Error::AmbiguousCacheBackend` if `kind` is `None` and multiple backends are compiled in.
543    /// Returns an error if the selected backend fails to initialize.
544    pub async fn new(config: &CacheConfig, ttl: Option<u64>) -> Result<Self> {
545        match PersistentBackendKind::resolve(config.persistent_backend)? {
546            #[cfg(feature = "cache-json")]
547            PersistentBackendKind::Json => Ok(Self::Json(JsonFileCache::new(config.cache_dir.clone(), ttl).await?)),
548            #[cfg(feature = "cache-redb")]
549            PersistentBackendKind::Redb => Ok(Self::Redb(RedbFileCache::new(config.cache_dir.clone(), ttl).await?)),
550            #[cfg(feature = "cache-redis")]
551            PersistentBackendKind::Redis => {
552                let url = config.redis_url.as_deref().unwrap_or("redis://127.0.0.1/");
553                Ok(Self::Redis(
554                    RedisFileCache::new(url, config.cache_dir.clone(), ttl).await?,
555                ))
556            }
557        }
558    }
559}
560
561#[cfg(persistent_cache)]
562impl FileBackend for PersistentFileBackend {
563    async fn get_by_hash(&self, hash: &str) -> Result<Option<(CachedFile, PathBuf)>> {
564        delegate_to_backend!(self.get_by_hash(hash))
565    }
566
567    async fn get_by_video_and_format(&self, video_id: &str, format_id: &str) -> Result<Option<(CachedFile, PathBuf)>> {
568        delegate_to_backend!(self.get_by_video_and_format(video_id, format_id))
569    }
570
571    async fn get_by_video_and_preferences(
572        &self,
573        video_id: &str,
574        preferences: &FormatPreferences,
575    ) -> Result<Option<(CachedFile, PathBuf)>> {
576        delegate_to_backend!(self.get_by_video_and_preferences(video_id, preferences))
577    }
578
579    async fn put(&self, file: CachedFile, source_path: &std::path::Path) -> Result<PathBuf> {
580        delegate_to_backend!(self.put(file, source_path))
581    }
582
583    async fn remove(&self, id: &str) -> Result<()> {
584        delegate_to_backend!(self.remove(id))
585    }
586
587    async fn clean(&self) -> Result<()> {
588        delegate_to_backend!(self.clean())
589    }
590
591    async fn get_thumbnail_by_video_id(&self, video_id: &str) -> Result<Option<(CachedThumbnail, PathBuf)>> {
592        delegate_to_backend!(self.get_thumbnail_by_video_id(video_id))
593    }
594
595    async fn put_thumbnail(&self, thumbnail: CachedThumbnail, source_path: &std::path::Path) -> Result<PathBuf> {
596        delegate_to_backend!(self.put_thumbnail(thumbnail, source_path))
597    }
598
599    async fn get_subtitle_by_language(&self, video_id: &str, language: &str) -> Result<Option<(CachedFile, PathBuf)>> {
600        delegate_to_backend!(self.get_subtitle_by_language(video_id, language))
601    }
602}