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}