yt_dlp/cache/backend/redb/
video.rs1use std::path::PathBuf;
4use std::sync::Arc;
5
6use redb::{Database, ReadableDatabase, ReadableTable};
7
8use super::{DEFAULT_VIDEO_TTL, VIDEO_URL_INDEX, VIDEOS, clean_redb_table, url_hash};
9use crate::cache::backend::VideoBackend;
10use crate::cache::video::CachedVideo;
11use crate::error::Result;
12use crate::model::Video;
13use crate::utils::is_expired;
14
15#[derive(Debug, Clone)]
17pub struct RedbVideoCache {
18 db: Arc<Database>,
19 ttl: u64,
20}
21
22impl RedbVideoCache {
23 pub async fn new(cache_dir: PathBuf, ttl: Option<u64>) -> Result<Self> {
25 if !cache_dir.exists() {
26 tokio::fs::create_dir_all(&cache_dir).await?;
27 }
28
29 let db_path = cache_dir.join("videos.redb");
30 let db = tokio::task::spawn_blocking(move || Database::create(db_path))
31 .await
32 .map_err(|e| crate::error::Error::runtime("redb open", e))?
33 .map_err(|e| crate::error::Error::database("open videos.redb", e))?;
34
35 let db = Arc::new(db);
36 let db_init = db.clone();
37 tokio::task::spawn_blocking(move || {
38 let txn = db_init.begin_write()?;
39 {
40 let _ = txn.open_table(VIDEOS)?;
41 let _ = txn.open_table(VIDEO_URL_INDEX)?;
42 }
43 txn.commit()?;
44 Ok::<_, redb::Error>(())
45 })
46 .await
47 .map_err(|e| crate::error::Error::runtime("redb init", e))?
48 .map_err(|e| crate::error::Error::database("init videos table", e))?;
49
50 Ok(Self {
51 db,
52 ttl: ttl.unwrap_or(DEFAULT_VIDEO_TTL),
53 })
54 }
55}
56
57impl VideoBackend for RedbVideoCache {
58 async fn get(&self, url: &str) -> Result<Option<Video>> {
59 tracing::debug!(url = url, "🔍 Looking for video in redb cache by URL");
60
61 let db = self.db.clone();
62 let url_owned = url.to_string();
63 let ttl = self.ttl;
64
65 tokio::task::spawn_blocking(move || {
66 let txn = db
67 .begin_read()
68 .map_err(|e| crate::error::Error::database("read video", e))?;
69
70 let hash = url_hash(&url_owned);
71 let index = txn
72 .open_table(VIDEO_URL_INDEX)
73 .map_err(|e| crate::error::Error::database("open video url index", e))?;
74
75 if let Some(id_guard) = index
76 .get(hash.as_str())
77 .map_err(|e| crate::error::Error::database("read video url index", e))?
78 {
79 let id = id_guard.value();
80 let table = txn
81 .open_table(VIDEOS)
82 .map_err(|e| crate::error::Error::database("open videos table", e))?;
83
84 if let Some(entry) = table
85 .get(id)
86 .map_err(|e| crate::error::Error::database("get video by indexed id", e))?
87 {
88 let cached: CachedVideo = serde_json::from_slice(entry.value())?;
89 if !is_expired(cached.cached_at, ttl) {
90 return Ok(Some(cached.video()?));
91 }
92 }
93 }
94
95 Ok(None)
96 })
97 .await
98 .map_err(|e| crate::error::Error::runtime("redb get video", e))?
99 }
100
101 async fn put(&self, url: String, video: Video) -> Result<()> {
102 tracing::debug!(url = url, video_id = video.id, "⚙️ Caching video to redb backend");
103
104 let db = self.db.clone();
105 tokio::task::spawn_blocking(move || {
106 let hash = url_hash(&url);
107 let cached = CachedVideo::new(url, &video)?;
108 let bytes = serde_json::to_vec(&cached)?;
109 let txn = db
110 .begin_write()
111 .map_err(|e| crate::error::Error::database("write video", e))?;
112 {
113 let mut table = txn
114 .open_table(VIDEOS)
115 .map_err(|e| crate::error::Error::database("open videos table", e))?;
116 table
117 .insert(cached.id.as_str(), bytes.as_slice())
118 .map_err(|e| crate::error::Error::database("insert video", e))?;
119
120 let mut index = txn
121 .open_table(VIDEO_URL_INDEX)
122 .map_err(|e| crate::error::Error::database("open video url index", e))?;
123 index
124 .insert(hash.as_str(), cached.id.as_str())
125 .map_err(|e| crate::error::Error::database("insert video url index", e))?;
126 }
127 txn.commit()
128 .map_err(|e| crate::error::Error::database("commit video", e))?;
129 Ok(())
130 })
131 .await
132 .map_err(|e| crate::error::Error::runtime("redb put video", e))?
133 }
134
135 async fn remove(&self, url: &str) -> Result<()> {
136 tracing::debug!(url = url, "⚙️ Removing video from redb cache");
137
138 let db = self.db.clone();
139 let url_owned = url.to_string();
140
141 tokio::task::spawn_blocking(move || {
142 let hash = url_hash(&url_owned);
143 let txn = db
144 .begin_write()
145 .map_err(|e| crate::error::Error::database("write video remove", e))?;
146 {
147 let mut index = txn
148 .open_table(VIDEO_URL_INDEX)
149 .map_err(|e| crate::error::Error::database("open video url index", e))?;
150
151 let id = index
152 .get(hash.as_str())
153 .map_err(|e| crate::error::Error::database("read video url index", e))?
154 .map(|g| g.value().to_string());
155
156 if let Some(id) = id {
157 let mut table = txn
158 .open_table(VIDEOS)
159 .map_err(|e| crate::error::Error::database("open videos table", e))?;
160 table
161 .remove(id.as_str())
162 .map_err(|e| crate::error::Error::database("remove video", e))?;
163 drop(table);
164
165 index
166 .remove(hash.as_str())
167 .map_err(|e| crate::error::Error::database("remove video url index", e))?;
168 }
169 }
170 txn.commit()
171 .map_err(|e| crate::error::Error::database("commit video remove", e))?;
172 Ok(())
173 })
174 .await
175 .map_err(|e| crate::error::Error::runtime("redb remove video", e))?
176 }
177
178 async fn clean(&self) -> Result<()> {
179 let db = self.db.clone();
180 let ttl = self.ttl;
181
182 tokio::task::spawn_blocking(move || clean_redb_table(&db, VIDEOS, ttl, &std::path::PathBuf::new(), "video"))
183 .await
184 .map_err(|e| crate::error::Error::runtime("redb clean videos", e))?
185 }
186
187 async fn get_by_id(&self, id: &str) -> Result<CachedVideo> {
188 tracing::debug!(video_id = id, "🔍 Looking up video by ID in redb cache");
189
190 let db = self.db.clone();
191 let id_owned = id.to_string();
192 let ttl = self.ttl;
193
194 tokio::task::spawn_blocking(move || {
195 let txn = db
196 .begin_read()
197 .map_err(|e| crate::error::Error::database("read video by id", e))?;
198 let table = txn
199 .open_table(VIDEOS)
200 .map_err(|e| crate::error::Error::database("open videos table", e))?;
201
202 if let Some(entry) = table
203 .get(id_owned.as_str())
204 .map_err(|e| crate::error::Error::database("get video by id", e))?
205 {
206 let bytes = entry.value();
207 let cached: CachedVideo = serde_json::from_slice(bytes)?;
208 if !is_expired(cached.cached_at, ttl) {
209 return Ok(cached);
210 }
211 }
212
213 Err(crate::error::Error::cache_miss(format!("video:{}", id_owned)))
214 })
215 .await
216 .map_err(|e| crate::error::Error::runtime("redb get video by id", e))?
217 }
218}