v0.2.1
Download
yt-dlp Engine

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

    } 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)
}



function cookieInflightSuffix(cookies?: VideoInfoCookieContext): string {

  if (!cookies) return "";

  const browser = cookies.browserCookies ?? "";

  const file = cookies.cookieFile ?? "";

  if (!browser && !file) return "";

  return `\x1f${browser}\x1f${file}`;

}



function displayOnlyInflightSuffix(displayOnly: boolean): string {

  return displayOnly ? "\x1fdisplay" : "";

}



export function videoInfoFetchInflightKey(

  url: string,

  videoFormat: string,

  cookies?: VideoInfoCookieContext,

  displayOnly = false,

): string {

  const key = normalizeYouTubeUrlForCompare(url);

  if (!key) return "";
                crate::rf_log!(
                    "download.comments",
                    log::Level::Warn,
                    "comments sidecar write task failed: {:?}",
                    e
                );
            }
        }
    });
}

#[derive(Default, Clone)]
struct PlaylistDownloadProgressExtras {
    current_index: Option<u32>,
    total_items: Option<u32>,
    current_item_title: Option<String>,
}

fn parse_ytdlp_playlist_download_line(line: &str) -> Option<(u32, u32, Option<String>)> {
    if !line.contains("[download]") {
        return None;
    }
    let after = line
        .find("Downloading ")
        .map(|k| line[k + "Downloading ".len()..].trim_start())?;
    let (head, tail_raw) = match after.split_once(" - ") {
        Some((h, t)) => (h.trim(), Some(t.trim())),
        None => (after.trim(), None),
    };

    let head = ["video ", "item ", "entries ", "videos "]
        .into_iter()

Where it shows up

  • src-tauri/src/commands/downloader.rs spawn, progress IPC, finish events
  • downloadVideoInfoFetch.ts deduped invoke("get_video_info") with timeout
  • downloadFormat.ts for format strings shared by simulate and download
  • downloadQueueSlice.ts for queue state, hero fields, and watchdog hooks