Skip to main content

yt_dlp/cache/backend/
memory.rs

1//! In-memory Moka cache backend.
2//!
3//! This module provides in-memory cache implementations backed by Moka's async cache
4//! with built-in TTL eviction. Data is stored in RAM only and is not persisted between
5//! process restarts.
6
7use std::path::{Path, PathBuf};
8use std::time::Duration;
9
10use moka::future::Cache;
11
12use super::{DEFAULT_FILE_TTL, DEFAULT_PLAYLIST_TTL, DEFAULT_VIDEO_TTL, FileBackend, PlaylistBackend, VideoBackend};
13use crate::cache::playlist::CachedPlaylist;
14use crate::cache::video::{CachedFile, CachedThumbnail, CachedVideo};
15use crate::error::Result;
16use crate::model::Video;
17use crate::model::playlist::Playlist;
18use crate::model::selector::FormatPreferences;
19
20const VIDEO_CAPACITY: u64 = 512;
21const FILE_CAPACITY: u64 = 64;
22const THUMBNAIL_CAPACITY: u64 = 256;
23const PLAYLIST_CAPACITY: u64 = 128;
24
25/// In-memory Moka video cache.
26#[derive(Debug, Clone)]
27pub struct MokaVideoCache {
28    data: Cache<String, CachedVideo>,
29}
30
31impl MokaVideoCache {
32    /// Creates a new in-memory Moka video cache.
33    pub async fn new(_cache_dir: PathBuf, ttl: Option<u64>) -> Result<Self> {
34        let ttl_secs = ttl.unwrap_or(DEFAULT_VIDEO_TTL);
35
36        Ok(Self {
37            data: Cache::builder()
38                .max_capacity(VIDEO_CAPACITY)
39                .time_to_live(Duration::from_secs(ttl_secs))
40                .build(),
41        })
42    }
43}
44
45impl VideoBackend for MokaVideoCache {
46    async fn get(&self, url: &str) -> Result<Option<Video>> {
47        tracing::debug!(url = url, "🔍 Looking for video in memory cache by URL");
48
49        if let Some(cached) = self.data.get(url).await {
50            return Ok(Some(cached.video()?));
51        }
52
53        Ok(None)
54    }
55
56    async fn put(&self, url: String, video: Video) -> Result<()> {
57        tracing::debug!(url = url, video_id = video.id, "⚙️ Caching video to memory backend");
58
59        let cached = CachedVideo::new(url.clone(), &video)?;
60        self.data.insert(url, cached).await;
61        Ok(())
62    }
63
64    async fn remove(&self, url: &str) -> Result<()> {
65        tracing::debug!(url = url, "⚙️ Removing video from memory cache");
66
67        self.data.remove(url).await;
68        Ok(())
69    }
70
71    async fn clean(&self) -> Result<()> {
72        self.data.run_pending_tasks().await;
73        Ok(())
74    }
75
76    async fn get_by_id(&self, id: &str) -> Result<CachedVideo> {
77        tracing::debug!(video_id = id, "🔍 Looking up video by ID in memory cache");
78
79        for (_, cached) in &self.data {
80            if cached.id == id {
81                return Ok(cached);
82            }
83        }
84
85        Err(crate::error::Error::cache_miss(format!("video:{}", id)))
86    }
87}
88
89/// In-memory Moka playlist cache.
90#[derive(Debug, Clone)]
91pub struct MokaPlaylistCache {
92    data: Cache<String, CachedPlaylist>,
93}
94
95impl MokaPlaylistCache {
96    /// Creates a new in-memory Moka playlist cache.
97    pub async fn new(_cache_dir: PathBuf, ttl: Option<u64>) -> Result<Self> {
98        let ttl_secs = ttl.unwrap_or(DEFAULT_PLAYLIST_TTL);
99
100        Ok(Self {
101            data: Cache::builder()
102                .max_capacity(PLAYLIST_CAPACITY)
103                .time_to_live(Duration::from_secs(ttl_secs))
104                .build(),
105        })
106    }
107}
108
109impl PlaylistBackend for MokaPlaylistCache {
110    async fn get(&self, url: &str) -> Result<Option<Playlist>> {
111        tracing::debug!(url = url, "🔍 Looking for playlist in memory cache by URL");
112
113        if let Some(cached) = self.data.get(url).await {
114            return Ok(Some(cached.playlist()?));
115        }
116
117        Ok(None)
118    }
119
120    async fn get_by_id(&self, id: &str) -> Result<Option<Playlist>> {
121        tracing::debug!(playlist_id = id, "🔍 Looking up playlist by ID in memory cache");
122
123        for (_, cached) in &self.data {
124            if cached.id == id {
125                return Ok(Some(cached.playlist()?));
126            }
127        }
128
129        Ok(None)
130    }
131
132    async fn put(&self, url: String, playlist: Playlist) -> Result<()> {
133        tracing::debug!(
134            url = url,
135            playlist_id = playlist.id,
136            "⚙️ Caching playlist to memory backend"
137        );
138
139        let cached = CachedPlaylist::from((url.clone(), playlist));
140        self.data.insert(url, cached).await;
141        Ok(())
142    }
143
144    async fn invalidate(&self, url: &str) -> Result<()> {
145        tracing::debug!(url = url, "⚙️ Invalidating playlist in memory cache");
146
147        self.data.remove(url).await;
148        Ok(())
149    }
150
151    async fn clean(&self) -> Result<()> {
152        self.data.run_pending_tasks().await;
153        Ok(())
154    }
155
156    async fn clear_all(&self) -> Result<()> {
157        tracing::debug!("⚙️ Clearing all playlists from memory cache");
158
159        self.data.invalidate_all();
160        self.data.run_pending_tasks().await;
161        Ok(())
162    }
163}
164
165/// In-memory Moka file cache.
166#[derive(Debug, Clone)]
167pub struct MokaFileCache {
168    files: Cache<String, CachedFile>,
169    thumbnails: Cache<String, CachedThumbnail>,
170}
171
172impl MokaFileCache {
173    /// Creates a new in-memory Moka file cache.
174    pub async fn new(_cache_dir: PathBuf, ttl: Option<u64>) -> Result<Self> {
175        let ttl_secs = ttl.unwrap_or(DEFAULT_FILE_TTL);
176        let ttl_duration = Duration::from_secs(ttl_secs);
177
178        Ok(Self {
179            files: Cache::builder()
180                .max_capacity(FILE_CAPACITY)
181                .time_to_live(ttl_duration)
182                .build(),
183            thumbnails: Cache::builder()
184                .max_capacity(THUMBNAIL_CAPACITY)
185                .time_to_live(ttl_duration)
186                .build(),
187        })
188    }
189}
190
191impl FileBackend for MokaFileCache {
192    async fn get_by_hash(&self, hash: &str) -> Result<Option<(CachedFile, PathBuf)>> {
193        tracing::debug!(hash = hash, "🔍 Looking for file in memory cache by hash");
194
195        Ok(self.files.get(hash).await.map(|cached| {
196            let path = PathBuf::from(&cached.relative_path);
197            (cached, path)
198        }))
199    }
200
201    async fn get_by_video_and_format(&self, video_id: &str, format_id: &str) -> Result<Option<(CachedFile, PathBuf)>> {
202        tracing::debug!(
203            video_id = video_id,
204            format_id = format_id,
205            "🔍 Looking for file by video and format in memory cache"
206        );
207
208        for (_, cached) in &self.files {
209            if cached.video_id.as_deref() == Some(video_id) && cached.format_id.as_deref() == Some(format_id) {
210                return Ok(Some((cached.clone(), PathBuf::from(&cached.relative_path))));
211            }
212        }
213
214        Ok(None)
215    }
216
217    async fn get_by_video_and_preferences(
218        &self,
219        video_id: &str,
220        preferences: &FormatPreferences,
221    ) -> Result<Option<(CachedFile, PathBuf)>> {
222        tracing::debug!(
223            video_id = video_id,
224            video_quality = ?preferences.video_quality,
225            audio_quality = ?preferences.audio_quality,
226            "🔍 Looking for file by preferences in memory cache"
227        );
228
229        for (_, cached) in &self.files {
230            if cached.video_id.as_deref() == Some(video_id) && cached.matches_preferences(preferences) {
231                return Ok(Some((cached.clone(), PathBuf::from(&cached.relative_path))));
232            }
233        }
234
235        Ok(None)
236    }
237
238    async fn put(&self, file: CachedFile, _source_path: &Path) -> Result<PathBuf> {
239        tracing::debug!(
240            filename = file.filename,
241            file_id = file.id,
242            "⚙️ Caching file metadata to memory backend"
243        );
244
245        let path = PathBuf::from(&file.relative_path);
246        self.files.insert(file.id.clone(), file).await;
247        Ok(path)
248    }
249
250    async fn remove(&self, id: &str) -> Result<()> {
251        tracing::debug!(file_id = id, "⚙️ Removing file from memory cache");
252
253        self.files.remove(id).await;
254        Ok(())
255    }
256
257    async fn clean(&self) -> Result<()> {
258        self.files.run_pending_tasks().await;
259        self.thumbnails.run_pending_tasks().await;
260        Ok(())
261    }
262
263    async fn get_thumbnail_by_video_id(&self, video_id: &str) -> Result<Option<(CachedThumbnail, PathBuf)>> {
264        tracing::debug!(
265            video_id = video_id,
266            "🔍 Looking for thumbnail by video ID in memory cache"
267        );
268
269        for (_, cached) in &self.thumbnails {
270            if cached.video_id == video_id {
271                return Ok(Some((cached.clone(), PathBuf::from(&cached.relative_path))));
272            }
273        }
274
275        Ok(None)
276    }
277
278    async fn put_thumbnail(&self, thumbnail: CachedThumbnail, _source_path: &Path) -> Result<PathBuf> {
279        tracing::debug!(
280            thumbnail_id = thumbnail.id,
281            video_id = thumbnail.video_id,
282            "⚙️ Caching thumbnail metadata to memory backend"
283        );
284
285        let path = PathBuf::from(&thumbnail.relative_path);
286        self.thumbnails.insert(thumbnail.id.clone(), thumbnail).await;
287        Ok(path)
288    }
289
290    async fn get_subtitle_by_language(&self, video_id: &str, language: &str) -> Result<Option<(CachedFile, PathBuf)>> {
291        tracing::debug!(
292            video_id = video_id,
293            language = language,
294            "🔍 Looking for subtitle by language in memory cache"
295        );
296
297        for (_, cached) in &self.files {
298            if cached.video_id.as_deref() == Some(video_id) && cached.language_code.as_deref() == Some(language) {
299                return Ok(Some((cached.clone(), PathBuf::from(&cached.relative_path))));
300            }
301        }
302
303        Ok(None)
304    }
305}