- Python 100%
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> |
||
|---|---|---|
| config | ||
| json | ||
| src/yt_sonarr_downloader | ||
| .gitignore | ||
| CLAUDE.md | ||
| LICENSE | ||
| README.md | ||
| requirements.txt | ||
| setup.py | ||
| yt-sonarr-downloader.py | ||
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
qduring 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
urllibonly) - 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
qduring 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 optionalVISITOR_DATAto 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=2160for 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-cffi —
pip install curl-cffi— required for yt-dlp browser impersonation; without it some YouTube videos fail withPostprocessing: Conversion failed! - YouTube Data API v3 key
- Sonarr instance with API access
Installation
-
Clone the repository:
git clone <repository-url> cd yt-sonarr-downloader -
Install Python dependencies:
pip install -r requirements.txt -
Install yt-dlp:
pip install yt-dlp # or sudo apt install yt-dlp # On Ubuntu/Debian -
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:
- Go to Google Cloud Console
- Create a new project or select existing one
- Enable YouTube Data API v3
- Create credentials (API Key)
- Restrict the key to YouTube Data API v3
Sonarr API Key:
- Open Sonarr web interface
- Go to Settings → General
- 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 selectionEnter— confirm1–9— jump directly to that itemy— search YouTube directly (costs ~100 API quota units)s— skip this episodeq— 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.yis 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:
- Read the series path directly from Sonarr (no manual path config needed)
- Copy the file to the correct season subfolder (
Season 01/,Specials/, etc.) - Validate by comparing byte sizes — aborts without deleting on mismatch
- Delete the staged file only on success (
move) or leave it in place (copy) - Trigger a Sonarr
RescanSeriescommand and wait for it to complete - Trigger a
RenameSeriescommand 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.0–1.0) to skip manual selection for high-confidence matches:
AUTO_ACCEPT_CONFIDENCE = 0.85
When set, the tool runs in two passes:
- 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. - 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-stream — q only prevents the next episode from starting.
Post-Download Steps
With AUTO_ADD_TO_SONARR = false (default):
- Refresh Sonarr — Refresh the series in Sonarr to detect new files
- Import Files — Use Sonarr's manual import if naming doesn't match
- Rename Files — Sonarr can rename to match your naming scheme
With AUTO_ADD_TO_SONARR = move or copy, steps 1–3 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 srtunless you have setSUBTITLE_CONVERT_FORMATexplicitly. This is required because mp4 containers only support SRT-compatible subtitle tracks — without conversion, yt-dlp will fail withPostprocessing: Conversion failed!when YouTube servesttml,vtt, orsrv3subtitles. If you output to MKV you can override withSUBTITLE_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:
- Look up the episode in Sonarr even though it already has a file
- Fetch the existing file's runtime from Sonarr's media info
- Run the normal video search and show the selection menu
- 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:
- 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. - 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 aboveMIN_CONFIDENCEis 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_completenotification is sent ifNTFY_TOPICis 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:
- Stops attempting further downloads immediately — no more spinning through the queue
- Prints a clear warning with instructions to enable
USE_COOKIES - 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).
Cookie Authentication
Some YouTube videos are age-restricted or require sign-in to download. There are two ways to pass your browser session to yt-dlp.
Option A — Live browser cookies (recommended)
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:
- Export your browser cookies to
cookies.txt. See yt-dlp's cookie export guide for instructions. - 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.
- Follow the yt-dlp PO Token Guide to obtain your
PO_TOKEN(and optionallyVISITOR_DATA). - 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=truecan 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
requestscalls inYouTubeFetcher - Sonarr API — all
requestscalls inSonarrClient - yt-dlp — the
--proxyflag 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
RenameSeriescommand after import so files are renamed per your configured naming scheme - Quality selection —
MAX_VIDEO_HEIGHTcaps resolution (default 1080p, set to 2160 for 4K) - Graceful quit — Press
qduring 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 filter —
MIN_CONFIDENCEcontrols 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 pinning —
LANGUAGEsetshl=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 Auth —
SONARR_AUTH_USERNAME/SONARR_AUTH_PASSWORDfor reverse proxies that require HTTP Basic Auth in front of Sonarr - Proxy test command —
--test-proxychecks all three network paths (YouTube HTTPS, YouTube API, Sonarr API) and prints response times - Config test command —
--test-configvalidates 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-cffiis missing (yt-dlp impersonation) or yt-dlp is more than 2 months old - PO Token support —
PO_TOKEN/VISITOR_DATApassed as yt-dlp extractor-args to bypass YouTube bot-detection without a full cookie export - Batch processing —
--series-fileJSON 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
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes
- Push to the branch
- 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.