sync sonarr against a youtube channel
Find a file
david 3bd3ee0f62 fix: bypass livestream/duration filters for user-pinned video URLs
Videos fetched via a direct watch?v= URL are marked _user_pinned=True.
search_videos_for_episode skips all filtering (livestream heuristic,
duration check, confidence threshold) for pinned videos, since the user
has explicitly chosen that video and should not be blocked by heuristics.

Root cause: Critical Role VODs are long (>2h) and their descriptions
mention live streaming, causing is_likely_livestream() to filter them out.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 07:10:16 -04:00
config feat: add inter-download rate-limit delay to avoid YouTube bot-detection 2026-03-16 07:31:01 -04:00
json json keeper 2025-09-25 08:36:14 -04:00
src/yt_sonarr_downloader fix: bypass livestream/duration filters for user-pinned video URLs 2026-03-19 07:10:16 -04:00
.gitignore add USE_COOKIES support for yt-dlp cookie authentication 2026-03-16 06:36:15 -04:00
CLAUDE.md docs: document sidecar subtitle transfer in startup recovery 2026-03-18 21:20:43 -04:00
LICENSE license and readme 2025-09-25 08:41:25 -04:00
README.md docs: document sidecar subtitle transfer in startup recovery 2026-03-18 21:20:43 -04:00
requirements.txt restructing to proper project 2025-09-25 08:36:55 -04:00
setup.py restructing to proper project 2025-09-25 08:36:55 -04:00
yt-sonarr-downloader.py restructing to proper project 2025-09-25 08:36:55 -04:00

yt-sonarr-downloader

A Python tool that automatically downloads missing TV episodes from YouTube using Sonarr integration. Perfect for shows that aren't available through traditional means but are officially published on YouTube channels.

Features

  • Automatic Episode Detection — Finds missing monitored episodes in Sonarr
  • Smart YouTube Search — Matches episode titles to YouTube videos with confidence scoring
  • Arrow-Key Menus — Navigate results with /, jump with number keys, or use letter shortcuts; an "Other…" item expands all remaining matches
  • Confidence Match Scoring — Shows how closely each YouTube video matches the episode title
  • Livestream Filtering — Automatically skips live streams and long compilations
  • Interactive Search — Manual fallback search through the cached channel video list
  • YouTube Direct Search — When no cached match is found, search YouTube's full index directly; prefers results from the target channel, falls back to a broad search
  • Auto-Copy to Sonarr — After download, copies files into the correct season folder, validates, triggers a library rescan, then renames to match Sonarr's configured naming scheme
  • Auto-Accept Confidence — Set a confidence threshold to batch-download high-confidence matches automatically; low-confidence episodes are queued for interactive review
  • Graceful Quit — Press q during any download to finish that episode and its transfer cleanly, then stop
  • Retry on Failure — If a download fails, choose [r] retry, [d] pick a different video, [s] skip, or [q] quit inline
  • Import from Directory — Bring locally downloaded video files into Sonarr without re-downloading: matches files by S##E## code or title confidence, confirms interactively, then moves or copies to the correct season folder and triggers a rescan
  • Batch Mode — Process multiple series in one run from a JSON file; startup checks run once, per-series state resets between entries, and a summary table and notification are sent at the end
  • Push Notifications — ntfy notifications for session complete, bot-detection alerts, errors, and batch summaries; no new dependencies (uses urllib only)
  • Bot-Detection Guard — Detects YouTube's "Sign in to confirm you're not a bot" error, aborts remaining downloads immediately, and prints actionable instructions rather than spinning through every queued episode
  • Rate-Limit Pacing — Adds a configurable inter-download delay (default 12 s for guest sessions, 2 s with cookies) with a live countdown; press q during the pause to request a clean quit; enforces YouTube's documented rate limits automatically
  • HTTP Proxy Support — Route all YouTube API calls, Sonarr API calls, and yt-dlp downloads through an HTTP or SOCKS5 proxy with optional username/password authentication
  • Language Pinning — Set a preferred language so YouTube API responses and yt-dlp metadata come back in the right language regardless of which country the proxy exits in
  • PO Token Support — Supply a YouTube Proof-of-Origin token (PO_TOKEN) and optional VISITOR_DATA to bypass bot-detection without needing to export browser cookies
  • Startup Recovery — Detects files stranded in staging by a previous interrupted run and transfers them
  • Flexible YouTube Source — Accepts channel handles, channel IDs, playlist IDs, or full YouTube URLs; playlist mode fetches only that playlist's videos
  • Video Caching — Caches channel video lists to reduce API calls; channel ID reused from stale cache on refresh
  • Quota Visibility — Tracks YouTube API quota used per session; warns when approaching the daily limit
  • Quality Control — Downloads capped at 1080p by default; set MAX_VIDEO_HEIGHT=2160 for 4K
  • Subtitle Support — Download and embed subtitles via yt-dlp
  • File Ownership — Automatic chown for media server compatibility
  • Highly Configurable — INI file and environment variable support

Prerequisites

  • Python 3.7+
  • yt-dlp
  • curl-cffipip install curl-cffi — required for yt-dlp browser impersonation; without it some YouTube videos fail with Postprocessing: Conversion failed!
  • YouTube Data API v3 key
  • Sonarr instance with API access

Installation

  1. Clone the repository:

    git clone <repository-url>
    cd yt-sonarr-downloader
    
  2. Install Python dependencies:

    pip install -r requirements.txt
    
  3. Install yt-dlp:

    pip install yt-dlp
    # or
    sudo apt install yt-dlp  # On Ubuntu/Debian
    
  4. Create configuration:

    cp config/arr-config.ini.example config/arr-config.ini
    

Configuration

Edit config/arr-config.ini with your settings:

[DEFAULT]

# ── Sonarr ────────────────────────────────────────────────────────────────────
SONARR_URL = http://localhost:8989
SONARR_API_KEY = your_sonarr_api_key_here

# HTTP Basic Auth (only if your reverse proxy requires it — leave blank otherwise)
SONARR_AUTH_USERNAME =
SONARR_AUTH_PASSWORD =

# Seconds to wait for Sonarr's RescanSeries command before skipping rename.
# Increase for large libraries.
RESCAN_TIMEOUT = 120

# ── YouTube ───────────────────────────────────────────────────────────────────
YOUTUBE_API_KEY = your_youtube_api_key_here

# Language code for YouTube API responses and yt-dlp metadata (BCP-47, e.g. en, fr, de, ja).
# Useful when routing through a foreign-country proxy.
LANGUAGE = en

# ── Download ──────────────────────────────────────────────────────────────────
DOWNLOAD_DIR = ./downloads

# Maximum video resolution in pixels. 1080 = 1080p, 2160 = 4K.
# Ignored if --format is set in YT_DLP_DOWNLOAD_OPTS.
MAX_VIDEO_HEIGHT = 1080

# yt-dlp passthrough options (advanced). --format is managed via MAX_VIDEO_HEIGHT.
YT_DLP_DOWNLOAD_OPTS = --merge-output-format mp4

# Minimum download file size (yt-dlp notation, e.g. 50M). Leave empty to disable.
MIN_FILESIZE =

# File ownership after download/transfer (leave blank to skip chown).
CHOWN_USER =
CHOWN_GROUP =

# ── Auto-add to Sonarr ────────────────────────────────────────────────────────
# move = copy to Sonarr and delete staged file
# copy = copy to Sonarr and keep staged file
# false = disabled (default)
AUTO_ADD_TO_SONARR = false

# ── Matching & confidence ─────────────────────────────────────────────────────
# Auto-download threshold (0.0 = disabled, e.g. 0.85 = 85% confidence required).
AUTO_ACCEPT_CONFIDENCE = 0.0

# Minimum confidence score to surface a video in results at all.
MIN_CONFIDENCE = 0.35

# Skip videos shorter than this many seconds (0 = disabled). Default blocks YouTube Shorts.
MIN_DURATION_SECONDS = 61

# ── Rate limiting ─────────────────────────────────────────────────────────────
# Paces downloads to avoid YouTube bot-detection. Set false to disable entirely.
# Minimum enforced: 12s (guest session), 2s (authenticated with cookies).
DOWNLOAD_DELAY_ENABLED = true
DOWNLOAD_DELAY_SECONDS = 12

# Exit cleanly after 2 consecutive bot-detection failures (recommended).
# Set false to keep trying regardless.
EXIT_BOT_DETECTION = true

# ── Cookie authentication ─────────────────────────────────────────────────────
# Option A: read cookies live from a browser on every download (always fresh).
# Supported: chrome, firefox, edge, brave, chromium, safari (macOS only).
# Append a profile to target a specific account: chrome:Default, firefox:myprofile
# Takes precedence over USE_COOKIES when both are set.
COOKIES_FROM_BROWSER =

# Option B: use a cookies.txt file exported from your browser.
# Place cookies.txt in the project root directory (it is gitignored).
USE_COOKIES = false

# ── PO Token (bot-detection bypass) ──────────────────────────────────────────
# Alternative to cookies — no login required.
# Obtain via: https://github.com/yt-dlp/yt-dlp/wiki/PO-Token-Guide
# Accepted as bare token or web+TOKEN form (web+ is prepended automatically).
PO_TOKEN =
VISITOR_DATA =

# ── HTTP proxy ────────────────────────────────────────────────────────────────
# Routes YouTube API, Sonarr API, and yt-dlp through a proxy.
USE_PROXY = false
PROXY_URL =
PROXY_USERNAME =
PROXY_PASSWORD =

# ── Subtitles ─────────────────────────────────────────────────────────────────
DOWNLOAD_SUBTITLES = false          # manually uploaded subtitles
DOWNLOAD_AUTO_SUBTITLES = false     # auto-generated subtitles
SUBTITLE_LANGUAGES = en             # comma-separated language codes
SUBTITLE_FORMAT = best              # best, srt, vtt, ass, etc.
EMBED_SUBTITLES = false             # embed into video container
SUBTITLE_CONVERT_FORMAT =          # force subtitle conversion (leave blank for auto)

# ── Misc ──────────────────────────────────────────────────────────────────────
DEBUG_MODE = false
CHANNEL_CACHE_EXPIRES_HOURS = 24
CURL_TIMEOUT = 30
SKIP_LIVESTREAMS = true

All options can also be set as environment variables.

Getting API Keys

YouTube API Key:

  1. Go to Google Cloud Console
  2. Create a new project or select existing one
  3. Enable YouTube Data API v3
  4. Create credentials (API Key)
  5. Restrict the key to YouTube Data API v3

Sonarr API Key:

  1. Open Sonarr web interface
  2. Go to Settings → General
  3. Copy the API Key

Usage

Basic Usage

python yt-sonarr-downloader.py "<Series Name>" "<YouTube Channel or Playlist>"

Examples

Download missing Bluey episodes:

python yt-sonarr-downloader.py "Bluey (2018)" "@BlueyOfficialChannel"

Use a specific playlist instead of the full channel:

python yt-sonarr-downloader.py "Bluey (2018)" "PLxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Use a playlist URL:

python yt-sonarr-downloader.py "Bluey (2018)" "https://www.youtube.com/playlist?list=PLxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

With auto-add to Sonarr library (move staged file after download):

AUTO_ADD_TO_SONARR=move DOWNLOAD_DIR=/tmp/yt-staging python yt-sonarr-downloader.py "Bluey (2018)" "@BlueyOfficialChannel"

Custom configuration file:

python yt-sonarr-downloader.py "Series Name" "@ChannelName" --config /path/to/custom-config.ini

Only process Season 3:

python yt-sonarr-downloader.py "Series Name" "@ChannelName" --season 3

Only process one specific episode:

python yt-sonarr-downloader.py "Series Name" "@ChannelName" --season 2 --episode 5

Re-download an episode that already exists in Sonarr:

python yt-sonarr-downloader.py "Series Name" "@ChannelName" --season 2 --episode 5 --redownload

Re-download without confirmation prompt:

python yt-sonarr-downloader.py "Series Name" "@ChannelName" --season 2 --episode 5 --redownload --force

Preview what the auto-accept pass would download (no downloads):

python yt-sonarr-downloader.py "Series Name" "@ChannelName" --dry-run

Force refresh the channel video cache:

python yt-sonarr-downloader.py "Series Name" "@ChannelName" --refresh-cache

Delete the channel video cache without re-fetching:

python yt-sonarr-downloader.py "@ChannelName" --clear-cache

YouTube Source Formats

The second positional argument accepts any of:

Format Example
Channel handle @BlueyOfficialChannel
Channel URL https://www.youtube.com/@BlueyOfficialChannel
Raw channel ID UCiRBHBaqBUv9_5iHCFTydkQ
Channel ID URL https://www.youtube.com/channel/UCiRBH...
Playlist ID PLxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Playlist URL https://www.youtube.com/playlist?list=PL...

Using a playlist ID/URL fetches only videos in that playlist rather than the channel's full uploads list, which is useful for channels that organise episodes into season playlists. It also saves quota — the 100-unit channel handle lookup is skipped entirely.

CLI Options

Flag Description
--series-file PATH Process multiple series from a JSON batch file (see Batch Processing); series_title and youtube_uploader_id are not used alongside this flag
--import-dir PATH Import video files from a local directory into Sonarr (YouTube channel not required; transfer mode controlled by AUTO_ADD_TO_SONARR)
--season N Only process missing episodes from season N (also limits episode picker in --import-dir)
--episode N Only process episode N (use with --season)
--redownload Replace an existing Sonarr file — compares runtimes and prompts before deleting (requires --season and --episode)
--recheck-quality Find all episodes whose file resolution is below MAX_VIDEO_HEIGHT and attempt to upgrade them
--force Skip the replacement confirmation prompt (use with --redownload or --recheck-quality)
--dry-run Preview what would be downloaded/imported without transferring anything
--refresh-cache Force re-fetch channel videos even if the cache is still valid
--clear-cache Delete the cached channel video list and exit (series title not required)
--cache PATH Use a custom cache file path
--config PATH Use a custom INI config file
--test-proxy Test proxy connectivity against YouTube and Sonarr, then exit
--test-config Validate configuration and verify all external dependencies (yt-dlp, ffmpeg, Sonarr API, YouTube API), then exit

Navigation

Each episode search presents a menu of the top 4 confidence-ranked YouTube matches:

Found YouTube videos:
▶  1) Let's Play Hide and Seek! | Bluey  [ 95%]  3:26 · 23,464,269 views
   2) Hide and Seek with Bluey           [ 61%]  5:12 · 8,103,442 views
   3) Bluey: Hide & Seek (Full Episode)  [ 48%]  7:01 · 5,221,003 views
   4) Hide and Seek Games | Bluey Clips  [ 36%]  4:45 · 1,903,812 views
   5) Other…  (12 more matches)
  [↑↓] navigate  [Enter] select  [1-5] jump  [y] YouTube search  [s] skip  [q] quit
  • / — move selection
  • Enter — confirm
  • 19 — jump directly to that item
  • y — search YouTube directly (costs ~100 API quota units)
  • s — skip this episode
  • q — quit immediately

Selecting Other… opens a second menu listing all remaining scored matches. Inside that menu, b returns to the primary list and y opens a YouTube search.

The [xx%] badge shows match confidence against the Sonarr episode title:

  • Bright green ≥ 80% — strong match
  • Green ≥ 55% — good match
  • Yellow ≥ 35% — partial match
  • Red < 35% — weak match (filtered from results by default)

Scoring strips common stop words (and, for, the, with, …) so only meaningful terms count. Three components are combined:

  • Word overlap (50%) — fraction of significant episode words present anywhere in the video title
  • Phrase matching (30%) — fraction of consecutive significant word pairs from the episode title that also appear consecutively in the video title (stop words on the video side are stripped first so they don't break matches)
  • Literal sequence (20%) — bonus when the full episode title appears verbatim in the video title, rewarding exact matches over paraphrases

When No Match Is Found

If the cache search finds no videos for an episode, you are offered three options:

  [y] YouTube search  [i] interactive  [s] skip
  Choice:
  • y — YouTube search — Queries the YouTube Data API directly (costs ~100 quota units). Searches the target channel first; if that returns nothing it falls back to a broad YouTube-wide search. Results are shown in an arrow-key menu with a [✓ Channel] badge for on-channel videos and the uploader name for off-channel ones. Use this for older episodes that may not appear in the channel's playlist API. y is also available as a shortcut inside the primary and overflow video menus whenever the cached results aren't what you need.
  • i — Interactive search — Browse and search the locally cached channel video list using keywords, filters (recent, popular, long, short), or exact phrases.
  • s / Enter — Skip — Move on to the next episode.

Interactive Search Mode

When automatic matching fails, press i to enter interactive search:

Command Action
list All video titles (paginated)
recent 10 most recent videos
popular 10 most viewed videos
long Videos longer than 10 minutes
short Videos shorter than 5 minutes
back Return to episode selection
quit Exit program

Search terms are flexible — partial words, multiple words, or "quoted phrases" for exact matching.

Auto-Add to Sonarr

Set AUTO_ADD_TO_SONARR to automatically transfer each download to the correct Sonarr season folder:

Value Behaviour
false Disabled (default) — files stay in DOWNLOAD_DIR
move Copy to Sonarr, delete staged file after validation
copy Copy to Sonarr, keep staged file in DOWNLOAD_DIR

After each download the tool will:

  1. Read the series path directly from Sonarr (no manual path config needed)
  2. Copy the file to the correct season subfolder (Season 01/, Specials/, etc.)
  3. Validate by comparing byte sizes — aborts without deleting on mismatch
  4. Delete the staged file only on success (move) or leave it in place (copy)
  5. Trigger a Sonarr RescanSeries command and wait for it to complete
  6. Trigger a RenameSeries command so Sonarr applies its configured naming scheme

The transfer runs synchronously after each download, so output stays clean and sequential.

AUTO_ADD_TO_SONARR = move    # transfer and clean up staged file
DOWNLOAD_DIR = /tmp/yt-staging

Startup Recovery

On each run, the tool scans DOWNLOAD_DIR for files from a previous interrupted run that belong to the current series and are still missing in Sonarr. These are transferred immediately before episode processing begins — no re-download needed.

Sidecar subtitle files (.srt, .vtt, .ass, etc.) with the same filename stem are included in the transfer automatically, so subtitle files stranded alongside a video are never left behind.

Files belonging to a different series are reported as warnings but never touched. There is no risk of a file being sent to the wrong show's Sonarr path — the series name embedded in each filename is always matched against the current session before any action is taken.

Auto-Accept Confidence

Set AUTO_ACCEPT_CONFIDENCE to a threshold (0.01.0) to skip manual selection for high-confidence matches:

AUTO_ACCEPT_CONFIDENCE = 0.85

When set, the tool runs in two passes:

  1. Pass 1 — Automatic: All episodes whose top YouTube match meets or exceeds the threshold are downloaded without any prompts. These are highlighted in cyan as ✔ Auto-accepted [85%]: Video Title. If a Pass 1 download fails, the episode is automatically queued for interactive review in Pass 2.
  2. Pass 2 — Interactive: Any remaining episodes (no match found, best match below threshold, or failed auto-download) are presented for manual review as normal.

At the end of the run a summary lists every auto-accepted episode alongside the matched video title and its confidence score. Set to 0.0 (default) to disable and always prompt.

Graceful Quit During Download

While the In progress… spinner is running, press q (no Enter needed) to request a clean exit:

  • The spinner changes to ⚑ Quit requested — finishing this download first…
  • The current yt-dlp download completes normally
  • The Sonarr copy, validation, rescan, and rename all complete
  • The episode loop then exits — no further episodes are processed
  • If using AUTO_ACCEPT_CONFIDENCE, any remaining interactive-review episodes are also skipped

The download is never interrupted mid-streamq only prevents the next episode from starting.

Post-Download Steps

With AUTO_ADD_TO_SONARR = false (default):

  1. Refresh Sonarr — Refresh the series in Sonarr to detect new files
  2. Import Files — Use Sonarr's manual import if naming doesn't match
  3. Rename Files — Sonarr can rename to match your naming scheme

With AUTO_ADD_TO_SONARR = move or copy, steps 13 are handled automatically.

Video Quality

Downloads are capped at 1080p by default. The format selector is built automatically:

MAX_VIDEO_HEIGHT = 1080   # default — 1080p max
MAX_VIDEO_HEIGHT = 2160   # 4K/UHD

The tool tries the best available MP4 stream at or below the height limit, falling back through other containers if MP4 isn't available at that resolution.

If you need full control over the yt-dlp format string, set --format in YT_DLP_DOWNLOAD_OPTS — this disables the MAX_VIDEO_HEIGHT selector entirely:

YT_DLP_DOWNLOAD_OPTS = --format bestvideo[height<=720][ext=mp4]+bestaudio[ext=m4a]/best[height<=720] --merge-output-format mp4

Subtitle Configuration

DOWNLOAD_SUBTITLES = false         # Download manually uploaded subtitles
DOWNLOAD_AUTO_SUBTITLES = false    # Download auto-generated subtitles
SUBTITLE_LANGUAGES = en            # Comma-separated language codes
SUBTITLE_FORMAT = best             # srt, vtt, ass, best, etc.
EMBED_SUBTITLES = false            # Embed into video file vs separate files
SUBTITLE_CONVERT_FORMAT =          # Optional: convert all subs to this format

Note: When EMBED_SUBTITLES = true, the script automatically adds --convert-subs srt unless you have set SUBTITLE_CONVERT_FORMAT explicitly. This is required because mp4 containers only support SRT-compatible subtitle tracks — without conversion, yt-dlp will fail with Postprocessing: Conversion failed! when YouTube serves ttml, vtt, or srv3 subtitles. If you output to MKV you can override with SUBTITLE_CONVERT_FORMAT = ass.

Re-downloading an Episode

To replace an episode that is already in your Sonarr library, use --redownload together with --season and --episode:

python yt-sonarr-downloader.py "Critical Role" "@CriticalRole" --season 1 --episode 21 --redownload

The script will:

  1. Look up the episode in Sonarr even though it already has a file
  2. Fetch the existing file's runtime from Sonarr's media info
  3. Run the normal video search and show the selection menu
  4. After you pick a video, display a comparison before replacing:
  Replacing existing file
    Existing file runtime : 2:58:44
    Selected video runtime: 2:58:44
    Runtimes are very similar (0s difference) — this may be the same content.
  Replace existing file and re-download? [y/N]
  • If runtimes differ by ≤ 30 seconds, a warning is shown — this usually means it's the same video
  • Declining the prompt returns you to the selection menu to pick a different video
  • The existing file is only deleted immediately before the download begins — you can browse options first

Use --force to skip the confirmation prompt entirely:

python yt-sonarr-downloader.py "Critical Role" "@CriticalRole" --season 1 --episode 21 --redownload --force

Upgrading Episode Quality

Use --recheck-quality to find all episodes whose existing Sonarr file is below your configured MAX_VIDEO_HEIGHT and attempt to replace them with a higher quality version:

# Find all sub-1080p episodes and prompt before replacing each
python yt-sonarr-downloader.py "Critical Role" "@CriticalRole" --recheck-quality

# Limit to a specific season
python yt-sonarr-downloader.py "Critical Role" "@CriticalRole" --recheck-quality --season 1

# Fully automatic: auto-accept high-confidence matches, skip prompts
AUTO_ACCEPT_CONFIDENCE=0.90 python yt-sonarr-downloader.py "Critical Role" "@CriticalRole" --recheck-quality --force

The confirmation prompt shows the quality context alongside the runtime comparison:

  Replacing existing file
    Current quality  : 720p
    Target quality   : 1080p
    Existing runtime : 2:58:44
    Selected runtime : 2:58:44
    Runtimes are very similar (0s difference) — this may be the same content.
  Replace existing file and re-download? [y/N]

Quality is read from Sonarr's file metadata. Episodes where the resolution is unknown are skipped.

Importing Local Files

If you already have video files downloaded with another tool (browser extensions, standalone yt-dlp runs, etc.), --import-dir brings them into Sonarr without re-downloading anything.

# Dry-run — show what would be matched, transfer nothing (AUTO_ADD_TO_SONARR=false)
python yt-sonarr-downloader.py "Series Name" --import-dir /path/to/files

# Copy files to Sonarr, keep originals (AUTO_ADD_TO_SONARR=copy)
AUTO_ADD_TO_SONARR=copy python yt-sonarr-downloader.py "Series Name" --import-dir /path/to/files

# Move files to Sonarr, remove originals (AUTO_ADD_TO_SONARR=move)
AUTO_ADD_TO_SONARR=move python yt-sonarr-downloader.py "Series Name" --import-dir /path/to/files

The YouTube channel argument is not required when using --import-dir. The transfer mode is controlled by AUTO_ADD_TO_SONARR — set it to move or copy in your config file or as an environment variable. When AUTO_ADD_TO_SONARR=false (default), --import-dir runs as a dry-run.

Matching strategy

Each video file is matched to a Sonarr episode using a two-step approach:

  1. S##E## code — if the filename contains a season/episode code (e.g. S02E05, s1e3), the episode is looked up directly by code. No confidence scoring needed.
  2. Title confidence fallback — if no code is found (or the code doesn't exist in Sonarr), the series name and any S##E## fragments are stripped from the filename stem, and the remainder is scored against all episode titles using the same confidence_score() algorithm used during YouTube matching. The best match at or above MIN_CONFIDENCE is proposed.

Per-file confirmation

After matching, an interactive confirmation menu is shown for each file:

Importing: The Temple Showdown.mp4
  Matched (title): S01E11 - The Temple Showdown  [98%]

  ▶  1) ✓ Accept match
     2) Pick different episode
     3) Skip this file
  [↑↓] navigate  [Enter] select  [q] quit

Selecting Pick different episode opens a second menu listing every Sonarr episode for the series, so you can assign the file manually even when automatic matching fails entirely.

Transfer mode

The same AUTO_ADD_TO_SONARR setting that controls post-download behaviour also controls --import-dir:

AUTO_ADD_TO_SONARR Behaviour
false (default) Dry-run — shows matches, transfers nothing
copy Copies to Sonarr season folder, source file kept
move Copies to Sonarr season folder, source file removed on success

All modes create the target season directory if it doesn't exist, validate the transferred file by byte size (no data loss on mismatch), apply CHOWN_USER/CHOWN_GROUP if configured, and trigger a Sonarr RescanSeries + RenameSeries after transfer.

Use --season N to limit matching and episode selection to a specific season:

AUTO_ADD_TO_SONARR=move python yt-sonarr-downloader.py "Critical Role" --import-dir ~/Downloads/cr-s1 --season 1

Batch Processing

To process multiple series in a single run, create a JSON file listing each series and pass it with --series-file:

python yt-sonarr-downloader.py --series-file series.json
python yt-sonarr-downloader.py --series-file series.json --dry-run

Format:

[
  {"series": "Critical Role", "channel": "@criticalrole"},
  {"series": "Dimension 20", "channel": "@dimension20show", "season": 5},
  {"series": "My Anime", "import_dir": "/downloads/anime", "season": 1}
]

Each entry requires "series" plus either "channel" (YouTube handle/ID/playlist) or "import_dir" (local directory for --import-dir mode). Optional per-entry keys: "season", "episode", "redownload", "force", "recheck_quality".

  • Startup checks (yt-dlp, Sonarr, YouTube API) run once before the first series
  • Per-series state resets between entries — a bot-detection abort on one series does not affect the next
  • A summary table is printed after all series, showing ✓/✗ per series and total episode count
  • An ntfy batch_complete notification is sent if NTFY_TOPIC is configured

Push Notifications (ntfy)

ntfy is a free, self-hostable push notification service. Configure it to receive alerts on your phone or desktop when downloads finish, fail, or are blocked.

Config:

NTFY_URL      = https://ntfy.sh        # or your self-hosted instance
NTFY_TOPIC    =                        # required — e.g. "my-sonarr-dl"
NTFY_TOKEN    =                        # optional Bearer token for protected topics
NTFY_PRIORITY = default                # min / low / default / high / urgent

Notifications are sent for:

Event Priority Sent when
Session complete default At least 1 episode was downloaded in the session
Bot-detection abort high EXIT_BOT_DETECTION=true and 2 consecutive failures
Series error high Unhandled exception during a series run
Batch complete default After --series-file finishes all entries

Session-complete is silent when 0 episodes were downloaded — no noise for clean-slate runs.

Set NTFY_TOPIC to your topic name only (not a full URL). The server URL is configured separately via NTFY_URL.

Rate Limiting and Bot-Detection

YouTube enforces download rate limits on its video API:

  • Guest sessions (no cookies): ~300 videos/hour (~12 s per download)
  • Authenticated sessions (USE_COOKIES=true): ~2000 videos/hour (~2 s per download)

By default the tool adds a pause between each yt-dlp invocation and displays a live countdown:

⏳ Rate-limit pause — next download in 8s…

The minimum delay is enforced automatically based on whether you are using cookies — setting DOWNLOAD_DELAY_SECONDS lower than the floor just silently clamps it up. To disable pacing entirely, set DOWNLOAD_DELAY_ENABLED = false.

DOWNLOAD_DELAY_ENABLED = true    # default — enable inter-download pacing
DOWNLOAD_DELAY_SECONDS = 12      # default — safe for guest sessions; set to 2 with USE_COOKIES=true

Bot-Detection Handling

If YouTube still detects bot-like activity (exit code 1 with "Sign in to confirm you're not a bot"), the tool:

  1. Stops attempting further downloads immediately — no more spinning through the queue
  2. Prints a clear warning with instructions to enable USE_COOKIES
  3. Reports how many episodes were skipped
  ⚠  YouTube bot-detection triggered — downloads are blocked.
  Enable USE_COOKIES=true and export your browser cookies to cookies.txt.

See Cookie Authentication for setup instructions.

Video Filtering

YouTube Shorts

Videos shorter than 61 seconds are skipped by default. This blocks YouTube Shorts (which are officially ≤60 s but sometimes slightly longer after processing):

MIN_DURATION_SECONDS = 61   # default — blocks Shorts
MIN_DURATION_SECONDS = 0    # disable — allow all lengths

Minimum File Size

Reject downloads that are too small (e.g. geo-blocked stubs):

MIN_FILESIZE = 50M   # skip files smaller than 50 MB

Uses yt-dlp size notation: 50M = 50 MB, 100k = 100 KB. Leave empty to disable (default).

Some YouTube videos are age-restricted or require sign-in to download. There are two ways to pass your browser session to yt-dlp.

COOKIES_FROM_BROWSER tells yt-dlp to read cookies directly from your browser's on-disk store every time it runs. Cookies are always fresh, so long runs don't expire mid-session.

# Any browser yt-dlp supports:
COOKIES_FROM_BROWSER = chrome
COOKIES_FROM_BROWSER = firefox
COOKIES_FROM_BROWSER = edge
COOKIES_FROM_BROWSER = brave
COOKIES_FROM_BROWSER = chromium
COOKIES_FROM_BROWSER = safari      # macOS only

Targeting a specific profile (useful if you have multiple accounts):

# Chrome — profile folder name from ~/.config/google-chrome/ (Linux)
#          or %APPDATA%\Google\Chrome\User Data\ (Windows)
COOKIES_FROM_BROWSER = chrome:Default
COOKIES_FROM_BROWSER = chrome:Profile 1

# Firefox — profile name from ~/.mozilla/firefox/ (or about:profiles in Firefox)
COOKIES_FROM_BROWSER = firefox:your-profile-name

# Firefox with a Multi-Account Container
COOKIES_FROM_BROWSER = firefox:your-profile-name::YouTube

Private/incognito windows are not supported. Private sessions keep cookies in memory only — they are never written to disk, so no external tool can read them. Use a regular browser window (optionally a dedicated profile) logged into the YouTube account you want.

You can verify the setting works before a full run:

python yt-sonarr-downloader.py --test-config

Option B — Exported cookies.txt

Export cookies once from your browser and place the file in the project root:

  1. Export your browser cookies to cookies.txt. See yt-dlp's cookie export guide for instructions.
  2. Enable in your config:
USE_COOKIES = true

The cookies.txt file is listed in .gitignore and will not be committed. Note that exported cookies expire — if you hit bot-detection errors mid-run, re-export and restart. For long automated runs, Option A is more reliable.

Note: Both options drop the rate-limit floor from 12 s to 2 s automatically, matching YouTube's higher authenticated session limit (~2000 videos/hour).

PO Token

YouTube may block yt-dlp with a "Sign in to confirm you're not a bot" error even when cookies are not an option. A Proof-of-Origin (PO) token tells YouTube the request came from a real browser session without requiring a full cookie export.

  1. Follow the yt-dlp PO Token Guide to obtain your PO_TOKEN (and optionally VISITOR_DATA).
  2. Add them to your config:
PO_TOKEN = web+AqABAfkEAr...    # bare token also accepted — web+ is prepended automatically
VISITOR_DATA = CgtfXzl4...      # optional but recommended for authenticated sessions

All YouTube extractor arguments (lang, po_token, visitor_data) are consolidated into a single --extractor-args "youtube:..." flag passed to every yt-dlp invocation. PO_TOKEN and VISITOR_DATA can also be set as environment variables.

Tip: PO tokens and USE_COOKIES=true can be used together — yt-dlp will use both.

Sonarr Basic Authentication

If your Sonarr instance is behind a reverse proxy that requires HTTP Basic Auth, set:

SONARR_AUTH_USERNAME = myuser
SONARR_AUTH_PASSWORD = mypassword

The credentials are set once on the requests.Session so every Sonarr API call (series list, episode fetch, rescan, rename, file delete, etc.) automatically includes the Authorization: Basic … header. Startup logs Sonarr Basic Auth: enabled (user: '...') when configured.

This is separate from SONARR_API_KEY, which is Sonarr's own application-level authentication. Both can be set at the same time.

HTTP Proxy

Route all network traffic through an HTTP or SOCKS5 proxy — useful when your download machine is behind a corporate proxy, a VPN, or a specific exit-country proxy.

USE_PROXY = true
PROXY_URL = http://proxy.example.com:8080   # or socks5://10.0.0.1:1080
PROXY_USERNAME = alice                       # leave blank if no auth required
PROXY_PASSWORD = s3cr3t

All three network consumers are covered:

  • YouTube Data API — all requests calls in YouTubeFetcher
  • Sonarr API — all requests calls in SonarrClient
  • yt-dlp — the --proxy flag is passed to every download invocation

Credentials are percent-encoded internally so special characters in passwords are handled correctly. The startup log prints the proxy URL (without credentials) and (authenticated) when a username is set.

Use --test-proxy to verify the configuration before running a full download session:

python yt-sonarr-downloader.py --test-proxy

This checks all three network paths (YouTube HTTPS, YouTube Data API, Sonarr API) and prints response times or error messages for each. No series title or channel is required.

Language

When routing through a proxy in a non-English-speaking country, YouTube may return localised titles and metadata. Pin the language to keep results consistent:

LANGUAGE = en   # default; any BCP-47 code works: fr, de, ja, pt, etc.

This sets hl={language} on every YouTube Data API v3 request (via session-level params — no per-call changes needed) and passes --extractor-args "youtube:lang={language}" to every yt-dlp invocation so the YouTube extractor requests the correct language version of the page.

Self-Healing Config

On every startup, the INI file is automatically kept in sync with the current set of known options:

  • Missing options are appended with their default values under a # --- Auto-added missing options (defaults) --- header so new features work immediately
  • Removed/deprecated options are commented out in-place with a # [DEPRECATED] prefix so they are preserved for reference but no longer loaded

Both changes are reported at [INFO] level in the startup output.

Project Structure

yt-sonarr-downloader/
├── yt-sonarr-downloader.py          # Entry point
├── config/
│   └── arr-config.ini.example
├── json/                            # Video cache directory
└── src/yt_sonarr_downloader/
    ├── main.py                      # Orchestrator + CLI
    ├── config.py                    # Config loading
    ├── logger.py                    # Coloured logger
    ├── notify.py                    # ntfy push notifications
    ├── ui.py                        # Colors, ArrowMenu, confidence scoring
    ├── youtube/
    │   ├── fetcher.py               # YouTube Data API v3
    │   └── cache.py                 # Channel video cache
    ├── sonarr/
    │   └── client.py                # Sonarr v3 API client
    ├── search/
    │   ├── searcher.py              # Title matching + confidence scores
    │   └── interactive.py           # Interactive fallback search
    └── download/
        └── downloader.py            # yt-dlp wrapper + Sonarr transfer

Troubleshooting

Problem Fix
ModuleNotFoundError Run from the project root directory
YouTube API quota exceeded Wait for reset or use a second API key
No videos found in cache Press y to search YouTube directly (finds older videos); or i for interactive cache search
YouTube search quota exceeded Each y search costs ~100 units; daily default is 10,000. Wait for reset or use a second API key
Permission denied on download Ensure DOWNLOAD_DIR is writable
yt-dlp errors Run yt-dlp -U to update
Files stuck in staging Re-run against the same series — they'll be transferred automatically on startup
"Sign in to confirm you're not a bot" Enable USE_COOKIES=true and export browser cookies to cookies.txt (see Cookie Authentication); or set PO_TOKEN / VISITOR_DATA (see PO Token); also ensure DOWNLOAD_DELAY_ENABLED=true
Proxy connection errors Run --test-proxy to diagnose which of the three network paths is failing
Sonarr returns 401 Unauthorized Set SONARR_AUTH_USERNAME / SONARR_AUTH_PASSWORD if your reverse proxy requires Basic Auth
Postprocessing: Conversion failed! Install curl-cffi (pip install curl-cffi) so yt-dlp can impersonate a browser; also run yt-dlp -U to update
Startup warns about outdated yt-dlp Run yt-dlp -U on the machine running the script

Debug Mode:

DEBUG_MODE="true" python yt-sonarr-downloader.py "Series Name" "@Channel"

Roadmap / Possible Improvements

  • Auto-detect series paths — Read download locations directly from Sonarr
  • Auto-refresh — Trigger Sonarr library refresh after downloads
  • Subtitle support — Download and embed subtitles when available
  • Auto-accept confidence — Batch-download high-confidence matches; review the rest interactively
  • Smart renaming — Trigger Sonarr's RenameSeries command after import so files are renamed per your configured naming scheme
  • Quality selectionMAX_VIDEO_HEIGHT caps resolution (default 1080p, set to 2160 for 4K)
  • Graceful quit — Press q during download to finish cleanly and stop after transfer
  • Retry on failure — Inline retry/reselect/skip prompt when a download fails
  • Quota visibility — Session quota tracking with warning when approaching the daily limit
  • Configurable confidence filterMIN_CONFIDENCE controls which videos are surfaced at all
  • Oldest-first ordering — Processes episodes starting from the oldest unaired, not in random Sonarr order
  • YouTube Shorts filter — Videos under 61 seconds are skipped by default (MIN_DURATION_SECONDS); configurable or disableable
  • Minimum filesize — Pass a minimum download size to yt-dlp (MIN_FILESIZE, e.g. 50M)
  • Self-healing config — INI file is auto-updated on startup: missing options are appended with defaults; deprecated options are commented out
  • Playlist support — Use a specific playlist ID or URL instead of the full channel uploads list
  • Cookie authentication — Pass browser cookies to yt-dlp for age-restricted / bot-protected videos (USE_COOKIES)
  • Rate-limit pacing — Configurable inter-download delay with live countdown; floor enforced per session type
  • Bot-detection abort — Detects "Sign in to confirm" errors, stops immediately, and prints actionable instructions
  • Import from directory — Bring locally downloaded files into Sonarr via S##E## code or title confidence matching; transfer mode shared with AUTO_ADD_TO_SONARR; per-file interactive confirmation
  • HTTP proxy with auth — Route all API calls and yt-dlp downloads through an HTTP/SOCKS5 proxy; credentials percent-encoded automatically
  • Language pinningLANGUAGE sets hl= on YouTube API calls and --extractor-args youtube:lang= on yt-dlp to keep metadata in the expected language regardless of proxy exit country
  • Sonarr Basic AuthSONARR_AUTH_USERNAME/SONARR_AUTH_PASSWORD for reverse proxies that require HTTP Basic Auth in front of Sonarr
  • Proxy test command--test-proxy checks all three network paths (YouTube HTTPS, YouTube API, Sonarr API) and prints response times
  • Config test command--test-config validates config fields, checks yt-dlp/ffmpeg on PATH, verifies download dir is writable, and pings both APIs
  • Startup dependency warnings — warns at launch if curl-cffi is missing (yt-dlp impersonation) or yt-dlp is more than 2 months old
  • PO Token supportPO_TOKEN / VISITOR_DATA passed as yt-dlp extractor-args to bypass YouTube bot-detection without a full cookie export
  • Batch processing--series-file JSON format; startup checks run once, per-series state resets, summary table + ntfy notification at end
  • Notification support — ntfy push notifications for session complete, bot-detection, errors, and batch summaries; self-hosted or ntfy.sh

Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes
  4. Push to the branch
  5. Open a Pull Request

License

This project is licensed under a License — see the LICENSE file for details.

Disclaimer

This tool is intended for downloading content that you have the right to access and download. Always respect copyright laws and YouTube's Terms of Service. Only download content that is officially published and available for download.