1use 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#[derive(Debug, Clone)]
27pub struct MokaVideoCache {
28 data: Cache<String, CachedVideo>,
29}
30
31impl MokaVideoCache {
32 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#[derive(Debug, Clone)]
91pub struct MokaPlaylistCache {
92 data: Cache<String, CachedPlaylist>,
93}
94
95impl MokaPlaylistCache {
96 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#[derive(Debug, Clone)]
167pub struct MokaFileCache {
168 files: Cache<String, CachedFile>,
169 thumbnails: Cache<String, CachedThumbnail>,
170}
171
172impl MokaFileCache {
173 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}