Skip to main content

yt_dlp/cache/backend/redb/
video.rs

1//! Redb video cache backend.
2
3use 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/// Redb-backed video cache.
16#[derive(Debug, Clone)]
17pub struct RedbVideoCache {
18    db: Arc<Database>,
19    ttl: u64,
20}
21
22impl RedbVideoCache {
23    /// Creates a new redb video cache.
24    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}