} else {
format!(
"Metadata fetch failed (without cookies: {}; with cookies: {})",
get_video_info_simulate_failure_message(without_err),
get_video_info_simulate_failure_message(with_err)
)
}
}
fn format_music_ytdlp_cookie_fallback_failure(
without_err: &str,
with_err: &str,
browser: Option<&str>,
) -> String {
if ytdlp_stderr_is_cookie_export_failure(with_err) {
format!(
"{}\nWithout cookies: {}",
humanize_ytdlp_cookie_error(with_err, browser),
humanize_music_ytdlp_error(without_err)
)
} else {
format!(
"yt-dlp failed without cookies ({}); with cookies ({})",
humanize_music_ytdlp_error(without_err),
humanize_music_ytdlp_error(with_err)
)
}
}
fn format_download_job_failure(
error_log: &str,
code: Option<i32>,
browser_cookies: Option<&str>,
) -> String {
if ytdlp_stderr_is_missing_js_runtime(error_log) {
return format!("{}Download failed: no JavaScript runtime installed. Open Settings > Downloads to install Deno automatically.", JS_RUNTIME_MISSING_PREFIX);
}
if ytdlp_stderr_is_cookie_export_failure(error_log) {
let humanized = humanize_ytdlp_cookie_error(error_log, browser_cookies);
let trimmed = error_log.trim();
if trimmed.is_empty() || humanized.contains(trimmed) {
return humanized;
}
return format!("{}\n\nFull yt-dlp log:\n{}", humanized, trimmed);
}
if error_log.contains("HTTP Error 403") || error_log.contains("403: Forbidden") {
return format!(
"Download failed (HTTP 403): signed stream URL may have expired. Retry the job to resume with fresh cookies, or refresh cookies in Settings then retry. Full log:\n{}",
error_log.trim()
);
}
let trimmed = error_log.trim();
if trimmed.is_empty() {
format!("Download failed (exit code {:?})", code)
Built with
yt-dlp Engine
Every download and metadata preview shells out to a bundled yt-dlp binary. Rust owns the process; React shows the progress.
Sidecars live under src-tauri/binaries/yt-dlp-* and are declared in tauri.conf.json externalBin. ytdlp_shell_command resolves the right binary for the host OS, then start_download_job in downloader.rs builds args from your Settings format string, output template, cookie options, and audio-only flag.
Before you click Download, the hero calls get_video_info. That command runs two parallel -J -s simulates: one with your video format string, one with bestaudio[ext=m4a]/bestaudio for the audio size column. Partial success is fine (audio-only preview can succeed when video simulate fails). Results are cached on the frontend keyed by URL + format + cookies so rapid tab switches do not respawn yt-dlp.
Stdout parsing drives the UI. Lines containing [download] update percent, speed, and ETA in downloadQueueSlice. When bytes hit 100% but ffmpeg is still muxing, a processing phase latch keeps the row on "Processing…" until the child exits. A stall watchdog in TypeScript marks jobs failed if stdout goes quiet too long.
Finished jobs leave {title}.info.json (and often a .webp thumbnail) next to the media file. scan_gallery uses id from that sidecar for dedupe. Settings → Advanced can check upstream yt-dlp version and download a newer sidecar when one exists.
In the repo
Where it shows up
-
src-tauri/src/commands/downloader.rsspawn, progress IPC, finish events -
downloadVideoInfoFetch.tsdedupedinvoke("get_video_info")with timeout -
downloadFormat.tsfor format strings shared by simulate and download -
downloadQueueSlice.tsfor queue state, hero fields, and watchdog hooks