Skip to content

feat(downloader): YouTube cookie auth for age-restricted videos #40

@Shironex

Description

@Shironex

Background

Users are reporting download and preview failures on age-restricted YouTube videos. The root cause was confirmed from a verbose yt-dlp -v run:

android_vr player response playability status: LOGIN_REQUIRED
web_embedded player response playability status: UNPLAYABLE
web_safari player response playability status: LOGIN_REQUIRED
ERROR: [youtube] <id>: Sign in to confirm your age.

yt-dlp already cycles through all its client impersonations (android_vr, web_embedded, web_safari) — every one is bounced by YouTube because the video requires a logged-in, age-verified session. This affects both the preview flow (downloader:get-stream-url) and the full download flow (downloader:download), and it's the dominant "works for some, fails for others" failure class in 2026.

As a quick fix we now classify the failure and surface a clear translated error ("This video is age-restricted…") in the UI. See the commit attached to this issue. The quick fix does not unlock the videos — it only tells the user why they failed. This issue tracks the real fix.

Proposed solution — browser cookie import

yt-dlp supports reading cookies directly from installed browsers via --cookies-from-browser BROWSER. If the user is already signed into YouTube in that browser and has confirmed their age once, yt-dlp pulls the session cookie automatically. No manual export, no cookies.txt wrangling. This is the standard pattern for desktop yt-dlp wrappers.

Supported browsers: firefox, chrome, edge, brave, safari (macOS only), chromium, opera, vivaldi.

Scope

  • Add a "YouTube authentication" control to DownloadsSection.tsx (Settings → Downloads) with options:
    • None (default, current behavior)
    • Firefox / Chrome / Edge / Brave / Safari (macOS only)
    • Cookies file… → file picker for a cookies.txt (power-user fallback)
  • Persist to electron-store under downloads.youtubeAuth: { kind: 'none' | 'browser' | 'file'; browser?: string; cookiesPath?: string }.
  • New IPC handlers downloader:get-youtube-auth / downloader:set-youtube-auth.
  • Build a shared helper getYouTubeAuthArgs(): string[] in apps/desktop/src/main/ipc/downloader.ts that reads the setting and returns the appropriate yt-dlp flags ([], ['--cookies-from-browser', 'firefox'], or ['--cookies', '<path>']).
  • Splice it into all three yt-dlp invocation sites:
    • downloader:search (so search doesn't hide age-gated results)
    • downloader:get-stream-url (fixes preview)
    • downloader:download (fixes download)
  • Update classifyYtDlpFailure age-restriction message to point the user at Settings when auth is not configured, and to a different "cookies may be stale / browser locked" message when it is configured.
  • Add i18n strings (EN + PL) for all new Settings labels, dropdown options, error hints, and help text. Use namespace settings for labels and toast for error messages, consistent with the rest of the app.
  • Tests:
    • Unit tests for getYouTubeAuthArgs() covering all three kind values
    • IPC handler tests asserting the args are spliced in based on store state
    • A test for the "cookies configured but still failing" error branch

Caveats to surface in the UI

  • Chromium-family browsers (Chrome, Edge, Brave, Opera, Vivaldi) on Windows encrypt cookies with DPAPI and hold an exclusive lock on the cookies SQLite DB. yt-dlp can read them, but the browser must be closed at the time of the call. UI should warn the user: "Close Chrome before downloading, or use Firefox to avoid this."
  • Firefox has no such lock — if it's installed, recommend it as the default.
  • On macOS, Safari works out of the box but requires Full Disk Access permission.
  • On Linux, only Firefox and Chromium are commonly supported — detection should gate the options list by platform.

UX details

  • Default to None — do not touch user cookies without consent.
  • On first save with a non-None option, run a silent validation call (yt-dlp --cookies-from-browser X --simulate --skip-download https://www.youtube.com/) to confirm yt-dlp can actually read the cookies, and show an inline success/error indicator.
  • On failure during download, if auth is configured and yt-dlp still returns a LOGIN_REQUIRED or age-restricted error, prompt: "YouTube cookies appear to be stale or the browser is running. Close {browser} and retry, or refresh your YouTube sign-in."
  • Consider adding a small inline "Fix this" deep-link from the age-restriction toast straight to the new Settings row.

Out of scope (for this issue)

  • PO Token provider plugin support — yt-dlp debug output shows PO Token Providers: none. Some SABR-only videos need a PO token even with cookies. Adding a PO-token provider requires bundling a plugin or integrating yt-dlp-ejs. File as a separate issue once we see it bite users in the wild.
  • Cross-platform cookie sync / shared profile — some users run multiple browsers; picking one is fine for v1.
  • Auto-detect installed browsers — v1 can just list a static set and let the user pick.

Related quick-fix (already landed)

The diagnostic and error-classification groundwork is already in place, so this issue only needs to add the cookie flags and the Settings UI on top:

  • classifyYtDlpFailure() + stable error codes (yt_dlp_age_restricted, yt_dlp_video_unavailable, yt_dlp_no_audio_format) in apps/desktop/src/main/ipc/downloader.ts
  • translateYtDlpError() helper in apps/web/src/lib/ytdlpErrors.ts
  • EN + PL translations in apps/web/src/locales/{en,pl}/toast.json
  • Shared-logger fix so logger.error(..., err) actually serializes Error.message/stack instead of {}
  • Stderr capture on yt-dlp failure in get-stream-url (previously discarded)
  • Unit tests for classifyYtDlpFailure including the age-restriction detection path

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2-mediumFix soonarea:electronMain process, IPC, window managementenhancementNew feature or requesti18nInternationalization and localization

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions