Refresh fetches 8 channel feeds concurrently and mirrors thumbnails to disk so the offline feed still renders. Downloads are capped at 2 and shell out to yt-dlp with an absolute binary path, since GUI apps do not inherit a login shell PATH.
56 lines
1.6 KiB
Rust
56 lines
1.6 KiB
Rust
//! Local thumbnail cache.
|
|
//!
|
|
//! Feed thumbnails are remote `ytimg.com` URLs. Offline — the whole point of
|
|
//! this app — those fail to load and the feed becomes a wall of broken images.
|
|
//! So every thumbnail we learn about gets mirrored to disk.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
pub fn cache_dir(app_data: &Path) -> PathBuf {
|
|
app_data.join("thumbs")
|
|
}
|
|
|
|
/// Downloads one thumbnail if it isn't already cached. Returns its path.
|
|
pub async fn cache_one(
|
|
client: &reqwest::Client,
|
|
video_id: &str,
|
|
url: &str,
|
|
dir: &Path,
|
|
) -> Result<PathBuf, String> {
|
|
let dest = dir.join(format!("{video_id}.jpg"));
|
|
if dest.exists() {
|
|
return Ok(dest);
|
|
}
|
|
|
|
tokio::fs::create_dir_all(dir)
|
|
.await
|
|
.map_err(|e| format!("Cannot create thumbnail cache dir: {e}"))?;
|
|
|
|
let resp = client
|
|
.get(url)
|
|
.send()
|
|
.await
|
|
.map_err(|e| format!("Thumbnail request failed: {e}"))?;
|
|
|
|
if !resp.status().is_success() {
|
|
return Err(format!("Thumbnail returned HTTP {}", resp.status()));
|
|
}
|
|
|
|
let bytes = resp
|
|
.bytes()
|
|
.await
|
|
.map_err(|e| format!("Cannot read thumbnail body: {e}"))?;
|
|
|
|
// Write to a temp name then rename, so an interrupted download never leaves
|
|
// a half-written JPEG that later looks cached.
|
|
let tmp = dir.join(format!("{video_id}.jpg.part"));
|
|
tokio::fs::write(&tmp, &bytes)
|
|
.await
|
|
.map_err(|e| format!("Cannot write thumbnail: {e}"))?;
|
|
tokio::fs::rename(&tmp, &dest)
|
|
.await
|
|
.map_err(|e| format!("Cannot finalize thumbnail: {e}"))?;
|
|
|
|
Ok(dest)
|
|
}
|