Turn public YouTube videos, playlists, and channels into a local media library and publish them as live-TV-style channels in Tunarr.
TunarrTube is a self-hosted, local-first companion for Tunarr. It discovers videos with yt-dlp, stores its catalog in SQLite, downloads or streams media according to each source's retention policy, and keeps linked Tunarr channels up to date.
Important
TunarrTube is pre-1.0 software for one trusted operator. It has no login system. Keep it on localhost or behind an authenticated reverse proxy or VPN; do not expose it directly to the internet or an untrusted LAN.
- Import a public YouTube video, playlist, or channel.
- Select channel feeds: Videos, Shorts, archived Live streams, or all feeds.
- Preserve playlist order and detect additions or removals during manual or scheduled syncs.
- Choose permanent downloads, cache-on-first-play, or stream-on-demand per source.
- Produce stable MP4 files with JSON and NFO metadata sidecars.
- Create or update Tunarr Local Media sources and channels, with a check that Tunarr can actually serve the published guide before calling a publish successful (see Known issues below).
- Curate a Channel: a hand-picked, ordered lineup of clips (from an already-downloaded video, a pasted YouTube URL, or a local folder) with a burned-in overlay (artist/title/album, or a custom HTML/CSS template you design), published as its own Tunarr channel.
- Pick clips onto a Channel at scale with Content selection: AI (a free-text brief interpreted by your configured AI provider) or Smart/heuristic (no AI provider needed — freshness, spread across sources, and optional target length or per-source/artist grouping).
- AI Programming: turn a Source or Channel into a real dayparts/weekly or endless-rotation schedule via Anthropic, OpenAI, or a locally installed Claude Code CLI (no API key needed for that path) — including the AI Programming Director, a natural-language preview-before-publish workflow, and ready-made concept presets as a starting point.
- Translate media paths when TunarrTube and Tunarr use different host or container paths.
- Monitor background work, cache usage, and sanitized logs from the web interface, with an explicit Published/Publish incomplete status per channel — see Known issues.
- Recover interrupted jobs after a restart without deleting previously completed media.
- Run natively on macOS, Linux, or Windows, or use the included Docker configuration.
- A specific Tunarr bug can make Tunarr's entire server unresponsive after publishing. TunarrTube verifies each publish's guide before reporting success and won't leave a channel showing Published unless that check passes — but it can't fix the underlying bug, which lives in Tunarr itself. See chrisbenincasa/tunarr#2087 and "A channel shows 'Publish incomplete'..." below if you hit it.
- Single instance only. The job worker and scheduler keep their state in-process with no distributed locking — never run more than one TunarrTube process against the same SQLite database.
- A Tunarr channel can never stream a video live from YouTube, in any playback mode — Tunarr's local-media scanner only reads real files on disk. See the FAQ entry below.
- Claude Code AI Programming uses your real Claude usage, not a separate fee — see Claude Code Integration below before enabling it.
- No login system — see the notice above.
YouTube URL
│
▼
yt-dlp analysis ──► SQLite catalog ──► background job queue
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
MP4 + sidecars local cache live stream
│
▼
Tunarr Local Media ──► Tunarr channel
TunarrTube does not upload video metadata directly into Tunarr. Tunarr scans the shared media directory and reads the generated .nfo sidecars. It then matches scanned programs back to TunarrTube records using the YouTube ID recovered from each filename (the whole filename under the default naming scheme, or a trailing [videoId] under a custom one — see "Naming" below).
Docker is the simplest option. The TunarrTube image includes Node.js, yt-dlp, and FFmpeg.
docker compose --profile tunarr up -d --buildOpen:
- TunarrTube: http://localhost:3000
- Tunarr: http://localhost:8000
The included stack shares /media between both containers and configures TunarrTube to reach Tunarr at http://tunarr:8000.
Create .env beside compose.yaml:
TUNARRTUBE_TUNARR_URL=http://host.docker.internal:8000Then start only TunarrTube:
docker compose up -d --buildhost.docker.internal works with Docker Desktop and is configured through host-gateway for Linux. If Tunarr is in another Docker stack, connect both services to a shared Docker network and use Tunarr's service name instead.
The supplied Compose file persists:
| Data | Container path | Default volume |
|---|---|---|
| SQLite database and thumbnails | /config |
ytarr-config |
| Downloaded and cached media | /media |
ytarr-media |
| Channels overlay render cache and staged renders | /app/storage |
ytarr-storage |
| Optional Tunarr configuration | /config/tunarr |
tunarr-config |
/app/storage is a rebuildable cache (Channels overlay screenshots and staged render outputs) rather
than primary media, but the database keeps referencing paths under it until a render is republished to
a channel, so it's still persisted by default to avoid needlessly re-rendering after a container
recreation.
The ytarr-* volume names and /config/ytarr.db filename are legacy identifiers intentionally retained so upgrades continue using existing data.
To make downloaded files directly visible on the host, replace the ytarr-media:/media mounts in compose.yaml with the same bind mount for both services, for example:
- Linux/macOS:
/srv/tunarrtube-media:/media - Windows Docker Desktop:
D:/TunarrTube:/media
TunarrTube needs write access; Tunarr only needs read access. The TunarrTube container runs as UID/GID 1001.
Note
The Channels overlay-render feature needs a working headless Chromium in addition to yt-dlp/FFmpeg. The Docker image installs Debian's own chromium package for this (rather than Puppeteer's own bundled download) and points Puppeteer at it — no extra setup needed.
- Node.js 22 or newer
- A current
yt-dlp - FFmpeg
- A reachable Tunarr installation for channel publishing
- For the Channels overlay-render feature:
npm installdownloads Puppeteer's own bundled Chromium automatically (no separate browser install needed natively). If your environment blocks that download, setPUPPETEER_SKIP_DOWNLOAD=1and pointYTARR_PUPPETEER_EXECUTABLE_PATHat an existing Chrome/Chromium binary instead.
On macOS:
brew install yt-dlp ffmpegOn Linux, install FFmpeg using your distribution package manager. Install a current yt-dlp using its official installation instructions; distribution packages can lag behind YouTube changes.
On Windows, install Node.js 22, yt-dlp, and an FFmpeg Windows build. Add yt-dlp.exe and the FFmpeg bin directory to PATH, open a new PowerShell window, and verify:
node --version
yt-dlp --version
ffmpeg -versionIf either binary is outside PATH, configure its absolute path in .env:
TUNARRTUBE_YTDLP_PATH="/absolute/path/to/yt-dlp"
TUNARRTUBE_FFMPEG_PATH="/absolute/path/to/ffmpeg"npm install
npm run devOpen http://localhost:3000. For a production build:
npm run build
npm startBoth development and production startup generate Prisma Client and apply committed SQLite migrations automatically. npm start listens on 127.0.0.1; npm run start:lan deliberately listens on all interfaces and should only be used behind an authenticated boundary.
Two connections are required:
- TunarrTube must be able to reach Tunarr's HTTP API.
- Tunarr must be able to read the media files created by TunarrTube.
Use this matrix to choose the URL and media setup:
| TunarrTube | Tunarr | Tunarr URL from TunarrTube | Media setup |
|---|---|---|---|
| Native | Native | http://127.0.0.1:8000 |
Use the same absolute media directory in both applications. |
| Native | Docker | http://127.0.0.1:8000 |
Bind-mount the native media directory into Tunarr and add a path mapping if its container path differs. |
| Docker | Native | http://host.docker.internal:8000 |
Bind-mount host media at /media; map /media to the native path Tunarr sees. |
| Docker | Docker, included stack | http://tunarr:8000 |
Both containers use the shared /media volume; no mapping is needed. |
| Docker | Docker, separate stacks | Shared-network service URL or http://host.docker.internal:<port> |
Give both containers the same volume or bind mount; map paths if mount points differ. |
Configure the URL under Settings → Tunarr, then select Test Tunarr. Before changing anything, TunarrTube reads /openapi.json and verifies that the connected Tunarr version exposes the required API capabilities.
Add an ordered TunarrTube → Tunarr mapping in Settings when the same files have different absolute paths:
| TunarrTube sees | Tunarr sees |
|---|---|
/media |
/data/youtube |
/srv/tunarrtube |
/media |
/media |
D:\Tunarr Media |
Mappings use longest-prefix matching. Leave them empty when both applications see the same absolute path.
- Open Sources → Add Source.
- Paste a public HTTPS YouTube video, playlist, or channel URL.
- For a YouTube channel, choose its feed and history before analyzing: Latest 15, 25, 50, 75, 100, 250, or 500; Custom (1–5,000); or Unlimited. Then analyze it, choose the playback mode, and create the source. The history amount also applies to future syncs and does not delete older downloads.
- Wait for at least one download to complete. Permanent-download sources queue all discovered videos automatically.
- Open the source's Tunarr integration panel.
- Choose a channel name, optional channel number, and programming order.
- Select Create Tunarr Channel.
Publishing creates or reuses a Local Media source for the directory, waits for Tunarr's scan, and creates the channel lineup. Publishing again updates the linked channel instead of creating a duplicate.
A Channel (the entity under the Channels tab, distinct from a Source's own Tunarr channel above) lets you hand-pick clips and burn in a title/artist overlay before publishing:
- Open Templates once to confirm the built-in "Music Video Lower Third" template exists (it seeds itself automatically), or design your own with the visual drag-and-drop editor. The editor places text elements bound to clip metadata, and static PNG/GIF images (a logo bug, for example) — an animated GIF is baked in as a single still frame, it doesn't play in the render. Templates you create can be deleted from their editor page; built-in templates and ones still assigned to a channel can't be.
- Open Channels → New channel, name it, and pick a template.
- On the channel's page, add media: pick an already-downloaded video, paste a YouTube URL (downloaded through a Source created automatically for this channel — visible under Sources), or scan a local folder. For picking several clips at once from a Source's library, use the Content selection panel instead: AI takes a free-text brief ("only upbeat J-pop, skip anything acoustic") and asks the configured AI provider which clips fit; Smart needs no AI provider at all — it deterministically mixes in clips that are freshest, haven't been picked onto a channel before (or not in a while), spread across your chosen sources, and optionally close to a target clip length, or (in its "grouped by show or artist" style) plays each source/artist as its own back-to-back block.
- Each clip gets an artist automatically where possible: first from YouTube's own tags (when yt-dlp reports a video as official music content), otherwise from a background MusicBrainz/iTunes search applied only when it's confident enough (tune this in Settings). Edit any clip's title/artist/album directly, or use Look up to search and apply a match yourself — including when the automatic search wasn't confident enough to apply one.
- Select Render all, then wait for rendering to finish (check Queue). Open a rendered clip's page to preview it inline, see its file size/duration, copy its on-disk path, or Show in file manager to reveal the rendered
.mp4where it lives on disk. - Open the channel's Tunarr panel and select Publish to Tunarr.
This creates a second, independent Tunarr channel alongside any Source-based ones.
Instead of a fixed sort order, a Source or Channel can hand its programming to an AI provider (Anthropic or OpenAI): pick AI Programming as the programming order, optionally describe how you want it scheduled (e.g. "mornings should be calmer clips, evenings more upbeat"), and publish. TunarrTube asks the provider for a repeating daily schedule of named blocks (e.g. "Morning", "Primetime"), each with an ordered clip list, then creates one real Tunarr Custom Show per block and a native Tunarr time-slot schedule referencing them — this is Tunarr's own dayparting feature, not something TunarrTube simulates on top of a flat lineup.
- Requires
TUNARRTUBE_ANTHROPIC_API_KEYand/orOPENAI_API_KEYin the environment (see Configuration below) — no API key is ever stored in the database or shown in the UI. The Anthropic key uses aTUNARRTUBE_-prefixed name rather than the bareANTHROPIC_API_KEYso it can't collide with that variable's separate, unrelated meaning to theclaudeCLI itself (which treats its presence as an API-key login, overriding a claude.ai account login). As an alternative that needs no API key at all, see Claude Code Integration below. - Choose a provider globally in Settings, or override it per Source/Channel; leaving it as "auto" picks whichever single key is configured (having both set requires an explicit choice, so a request never silently runs against the wrong provider and bills the wrong account).
- Regenerating a schedule is a real, billed call to the configured provider. TunarrTube only calls it again when the candidate clips or your instructions actually changed since the last publish — an unrelated republish (a new video, a renumber) reuses the cached schedule and just updates the same Tunarr Custom Shows in place.
- Requires a Tunarr server whose Custom Shows API (
/api/custom-shows) is available — checked the same way every other required capability is, via/openapi.jsondiscovery, and only when AI Programming is actually selected. - Schedule style picks the shape of the schedule: Daily dayparts and Weekly broadcast are both fixed-time schedules (Tunarr's own "time slots"), the only difference being whether the AI plans one repeating day or a different lineup per weekday. Endless rotation has no fixed times at all — the AI groups clips with a relative weight and cooldown, and Tunarr shuffles them forever (its "random slots" engine), good for a channel that should just feel like a themed radio/video rotation.
- Concept preset is a shortcut for the instructions box (Balanced variety, Throwback/retro countdown, Late night chill, High energy/party, or Custom) — it just fills in a starting point you can still edit before publishing.
- Every AI-generated schedule is dry-run through Tunarr's own schedule preview endpoints before being published, and refused (not silently published) if the preview looks broken (a non-finite duration, or a block Tunarr's scheduler never actually reaches) rather than risk a channel with no real programming.
As an alternative to the Anthropic API, TunarrTube can run AI Programming through a locally installed Claude Code CLI, using your existing Claude Code sign-in — no Anthropic API key is required for this provider, and none of your Claude Code credentials ever pass through TunarrTube's own request/response bodies or get stored in its database. This is entirely optional: if you never enable it, TunarrTube behaves exactly like it always has, and if the claude executable isn't installed, every other feature keeps working normally.
To set it up:
- Install Claude Code on the same machine (or container) running TunarrTube. See claude.com/claude-code.
- Authenticate by running
claudeonce from a terminal and signing in. - In TunarrTube, open Settings → AI Assistant and turn on Enable Claude Code integration. If auto-detection can't find the binary, set an explicit Claude executable path.
- Click Test connection — this runs one real, short round-trip through the CLI (a fixed test prompt, never anything you typed) and reports whether it's genuinely working.
- Pick Claude Code (Local) as the AI provider — globally in Settings, or per Source/Channel — and use AI Programming (or the AI Programming Director, below) as usual.
A few things worth knowing:
- Claude Code integration is optional. Existing TunarrTube functionality does not depend on it, and "auto" provider resolution never silently picks it — you always have to select it explicitly.
- TunarrTube shells out to the
claudeCLI as a plain child process (claude -p --output-format json --max-turns 1 --restricted --tools "", with the prompt sent over stdin rather than as a command-line argument), with tool use disabled — it's used purely as a language-model inference process, never given filesystem, command-execution, or repository access, and it can never mutate TunarrTube's database or lineup directly. - Calls run with a configurable timeout (10–600s, default 120s) and are capped to a couple of concurrent invocations at a time, so TunarrTube never launches a pile of
claudeprocesses at once. - This uses your Claude subscription's usage, not a separate bill. Each call (even a trivial one) reports a nontrivial cost-equivalent in TunarrTube's logs — this reflects real usage against your plan's limits, not a fee on top of it, but it's not free/instant either. Use Test connection and Preview Schedule deliberately rather than repeatedly.
- If Claude Code isn't installed, isn't authenticated, or is disabled in Settings, you'll get a clear message ("Claude Code was not detected or is not authenticated. Install Claude Code and run
claudeonce from Terminal to sign in.") rather than a silent failure — and every other TunarrTube feature is entirely unaffected.
The AI Programming Director (shown on a Channel's Tunarr panel when AI Programming is selected) lets you describe a schedule in plain language — e.g. "Program this channel from 18:00 until midnight, playing episodes in order, and keep the schedule close to 30-minute blocks" — and preview exactly what the configured AI provider (Anthropic, OpenAI, or Claude Code) proposes before anything is saved:
- Type your request and click Preview Schedule. TunarrTube gathers this channel's own curated clips (never anything outside it), asks the AI for a schedule, and shows you the result: proposed blocks or rotation groups, computed start/end times and durations, and any warnings (clips that weren't used, overlapping blocks, or a request — like per-item filler — that TunarrTube's schedule shape doesn't support).
- Regenerate to try again, or Cancel to discard the preview without changing anything.
- Apply Schedule saves the same programming settings the manual AI Programming panel above it already uses, then republishes through the exact same Tunarr publish path as any other AI-scheduled channel — the AI never writes to the lineup or to Tunarr directly.
| Mode | Initial behavior | On browser playback | When publishing to Tunarr |
|---|---|---|---|
| Permanent download | Queues every discovered video | Plays the local file | Uses the existing local file |
| Cache on first play | Stores metadata only | Downloads into the shared cache | Materializes permanent local files first |
| Stream on demand | Stores metadata only | Proxies a short-lived YouTube stream | Materializes permanent local files first |
Cache limits default to 20 GB and 30 idle days and can be changed in Settings. Pinned, actively playing, and Tunarr-linked assets are protected from eviction.
A Tunarr channel never plays a live YouTube stream, regardless of playback mode. Tunarr's local-media scanner only reads real files sitting in a directory it's watching — it has no way to accept a remote URL per program, and the short-lived signed CDN URL TunarrTube resolves for
streamplayback expires far too quickly to serve a channel that might replay that item hours or days later. So publishing a Cache or Stream source still fully downloads (materializes) every video into the source's directory first, exactly like Permanent download — the playback mode only changes when and how long that copy is retained outside of Tunarr publishing, not whether Tunarr channels stream live. Live, on-demand streaming without ever writing a file only happens in TunarrTube's own in-app preview player, never in a published Tunarr channel.
Manual or scheduled synchronization detects new videos and marks missing memberships without deleting previously completed downloads. Channel sources can limit how much recent history is inspected. Run only one TunarrTube process against a given SQLite database; the worker and scheduler are intentionally single-instance.
TunarrTube exposes its HTTP API as an OpenAPI 3.1 document at /openapi.json. Import that URL into Postman, Insomnia, n8n, or an OpenAPI client generator. Operations have stable operationId values. JSON successes use { "data": ... }; JSON failures use { "error": { "code": "...", "message": "...", "details": ... } }.
There is no built-in API authentication. API operations can start background jobs, write media, change filesystem destinations, and mutate the configured Tunarr server. Keep TunarrTube on localhost or a trusted private network; for remote integrations, place it behind an authenticated reverse proxy or VPN with TLS.
curl http://localhost:3000/api/sources
curl -X POST http://localhost:3000/api/sources/analyze \
-H 'Content-Type: application/json' \
-d '{"url":"https://www.youtube.com/playlist?list=YOUR_PLAYLIST_ID"}'Copy .env.example to .env only when you need overrides. Environment settings seed the application on its first start; afterward, change the media directory and Tunarr URL in the Settings page. The Settings page also has a MusicBrainz contact email field — MusicBrainz's API usage policy requires a real contact identifier in the request User-Agent for the Channels metadata-lookup feature — plus toggles to enable/disable the MusicBrainz and iTunes lookups individually and an auto-apply confidence threshold (0-100) controlling how sure the automatic artist lookup (see above) needs to be before it applies a match on its own; anything scoring below the threshold is left for you to resolve manually via Look up.
| Variable | Purpose | Default |
|---|---|---|
DATABASE_URL |
Prisma SQLite connection | file:./ytarr.db |
TUNARRTUBE_YTDLP_PATH |
Absolute yt-dlp executable path |
Auto-discovered |
TUNARRTUBE_FFMPEG_PATH |
Absolute FFmpeg executable path | Auto-discovered |
TUNARRTUBE_MEDIA_DIR |
Initial media root | storage/media |
TUNARRTUBE_THUMBNAIL_DIR |
Thumbnail storage root | storage/thumbnails |
TUNARRTUBE_YTDLP_COOKIES |
Initial yt-dlp cookies file path (see "Age-restricted or sign-in-required videos" below) |
None (unauthenticated) |
TUNARRTUBE_TUNARR_URL |
Initial Tunarr base URL | http://127.0.0.1:8000 native; http://tunarr:8000 in Compose |
TUNARRTUBE_GENERAL_WORKERS |
How many lightweight non-download/cache/render jobs run concurrently | 3 |
TUNARRTUBE_DOWNLOAD_CONCURRENCY |
Shared media-fetch limit across queued downloads, cache fills, and Tunarr materialization | 1 |
TUNARRTUBE_RENDER_THREADS |
FFmpeg encoder/filter thread budget for the single render lane | 2 |
YTARR_FFPROBE_PATH |
Absolute ffprobe executable path (used by Channels rendering) |
Auto-discovered |
TUNARRTUBE_ANTHROPIC_API_KEY |
Enables Anthropic (Claude) as an AI Programming provider | None (feature unavailable if unset) |
OPENAI_API_KEY |
Enables OpenAI as an AI Programming provider | None (feature unavailable if unset) |
TUNARRTUBE_OPENAI_MODEL |
OpenAI model used for AI Programming | gpt-4o |
TUNARRTUBE_CLAUDE_PATH |
Absolute claude (Claude Code CLI) executable path, used when Settings' own path override is unset |
Auto-discovered |
YTARR_PUPPETEER_EXECUTABLE_PATH |
Absolute Chrome/Chromium path for Channels overlay rendering | Puppeteer's own bundled Chromium |
TUNARRTUBE_PORT |
Docker host port | 3000 |
TUNARR_PORT |
Optional Tunarr Docker host port | 8000 |
TUNARR_IMAGE |
Optional Tunarr image tag or digest | chrisbenincasa/tunarr:latest |
TZ |
Container timezone | UTC |
Former YTARR_* variables remain supported as lower-priority aliases for existing installations.
Native defaults:
- Database:
prisma/ytarr.db - Media:
storage/media/ - Thumbnails:
storage/thumbnails/ - Source files:
<media-root>/<source-directory>/<youtubeId>.mp4by default (see "Naming" below for custom filename/folder layouts) - Metadata: matching
<youtubeId>.jsonand<youtubeId>.nfofiles - Artwork: a matching
<youtubeId>-poster.jpg(or.png/.webp) file, mirrored from the video's thumbnail so Tunarr's guide and "now playing" screen have an image for it
Settings → Naming controls how downloaded files are named and organized, with a per-source override available on each source's own page:
- YouTube video ID (default) —
<mediaDirectory>/<youtubeId>.mp4, unchanged from every TunarrTube release before this setting existed. - Custom filename template — a template string using
{title},{channel},{date}(upload date,YYYY-MM-DD),{year}, and{videoId}, e.g.{channel} - {title}. A literal/in the template creates a subfolder, e.g.{channel}/{title}produces<mediaDirectory>/<channel>/<title>.mp4. The YouTube video ID is always appended in brackets (Title [videoId].mp4) when the template doesn't already include it, so Tunarr can still match the file and two same-titled videos never collide on disk. - TV show — an Emby/Plex/Jellyfin- and Tunarr "Shows"-scanner-compatible layout:
<mediaDirectory>/Season <upload year>/<source name> - S<year>E<episode> - <title> [videoId].mp4, with a matching Kodi<episodedetails>NFO (season/episode/plot/aired date, plus a<uniqueid type="youtube">carrying the YouTube ID), atvshow.nfoat the source's root, a<basename>-thumb.<ext>episode thumbnail, and a<basename>.info.jsonsidecar carrying the same metadata plus the assigned season/episode. Episode numbers are assigned once per video (first-assigned-wins per season) and never renumbered, so they stay stable even if older videos are discovered later. To have Tunarr itself recognize this layout as a TV show (rather than "Other Videos"), configure a Tunarr local media source of type "Shows" pointed at the source's directory — TunarrTube matches the right library automatically but doesn't create it for you.
Changing a naming setting only affects future downloads — already-downloaded files keep their existing names and locations (same rule as changing the media root above).
Back up the SQLite database and media root before upgrades.
- Queue shows running, retrying, recently completed work, and queued work in server-paginated pages. Polling pauses while the browser tab is hidden. A queued job (including one waiting to retry) can be cancelled outright or postponed to a later time (15 minutes up to a week) without losing its place; a failed or cancelled one can be retried, which requeues it fresh rather than resuming the old attempt. A running download, cache, sync, metadata, render, or Tunarr publish/refresh job can be stopped mid-flight — Stop immediately records cancellation (even for a stranded job with no live worker), then interrupts its process or network request; cancellation survives a restart. Publishing also passes Stop through to any media downloads it needs; a metadata-repair or thumbnail job has no interrupt point and finishes on its own instead. Cancelling or stopping a download or cache job is sticky — it stays cancelled through automatic syncs and Tunarr refreshes until you retry it (or play the video again, for Cache/Stream sources) or switch the source's playback mode to Permanent, which re-downloads everything not yet complete. The toolbar's Pause queue toggle stops the worker from picking up any new job (existing running work keeps going until it finishes or you stop it) — use it and Resume queue to hold everything for a while. Actual media fetches share one limiter across queued downloads, cache fills, and downloads initiated by Tunarr publishing (
TUNARRTUBE_DOWNLOAD_CONCURRENCY, default 1). FFmpeg renders use a separate single-job lane with a configurable thread budget (TUNARRTUBE_RENDER_THREADS, default 2); lightweight work uses the general lanes (TUNARRTUBE_GENERAL_WORKERS, default 3). - Cache shows used, pinned, protected, and evictable storage.
- Logs contains sanitized operational events. Signed YouTube/Googlevideo URLs and cookie flags are redacted before persistence. Entries older than the configured log retention (30 days by default, set in Settings) are purged automatically every hour; Purge old entries runs that same cleanup immediately, and Clear all empties the log table outright.
- Settings tests binary discovery and Tunarr connectivity, controls cache limits and log retention, sets the default downloaded filename/folder naming scheme (with a live preview), repairs older metadata sidecars, and previews path mappings.
Downloads are written to temporary paths and renamed into place only after yt-dlp and FFmpeg succeed. Interrupted jobs are requeued after restart only while attempts remain; jobs at their retry limit are marked failed and can be retried manually from Queue. Jobs normally retry up to three times — except one YouTube itself reports as permanently unavailable, which fails immediately without retrying (see "A download failed" below).
Open Settings to inspect the detected paths and versions. Add TUNARRTUBE_YTDLP_PATH or TUNARRTUBE_FFMPEG_PATH to .env if automatic discovery fails, then restart TunarrTube.
Current yt-dlp needs a JavaScript runtime (plus its yt-dlp-ejs solver scripts) to extract YouTube formats; without one, downloads fail with the warning above, and metadata jobs can intermittently fail with The page needs to be reloaded. TunarrTube itself always passes --js-runtimes node --remote-components ejs:github to every yt-dlp invocation (so it uses the Node.js it's already running on, and actually downloads the solver script instead of only warning it was skipped), and caches that download under its own persisted storage (/config in Docker, alongside the database; the project directory's storage/ folder for a host install) rather than yt-dlp's default ~/.cache, which isn't guaranteed to exist or be writable. The Docker image also ships yt-dlp[default] (the extra that includes yt-dlp-ejs) — rebuild and redeploy it if you see this on an older image. For a host install, pip install "yt-dlp[default]" (or install Deno, which yt-dlp auto-detects as an alternative runtime). See the yt-dlp EJS wiki.
Only public HTTPS URLs on supported YouTube hosts are accepted, including youtu.be. Private/unlisted, account-only, and members-only media are not supported — TunarrTube can't browse a signed-in account's library, only analyze a URL you already have. Age-restricted and bot-checked public videos are supported if you configure a cookies file (see "Age-restricted or sign-in-required videos" below); without one they're marked unavailable instead of failing repeatedly.
YouTube changes its extraction behavior regularly. In Settings, click Update next to yt-dlp to self-update it (no restart needed — TunarrTube re-discovers the binary on each use), then use Sync Now. An existing empty source does not need to be recreated. If yt-dlp was installed with a package manager (Homebrew, apt, pip), self-update refuses and reports the error; update it with that package manager instead.
Review Logs for the sanitized error. Check available disk space, filesystem permissions, and the installed yt-dlp/FFmpeg versions, then queue the video again.
If the error is YouTube reporting the video itself as private, deleted, or otherwise unavailable, TunarrTube records that on the video (visible as an "unavailable" badge on the source's video table) and stops retrying it automatically — including on future syncs — since a retry can never succeed. From there you can either Retry it from the Queue page (in case YouTube reinstates it later) or Remove it from the source's video table to drop it for good.
yt-dlp reports age-restricted and bot-checked videos with messages like Sign in to confirm your age or Sign in to confirm you're not a bot. TunarrTube recognizes these, marks the video "unavailable" with that explanation instead of retrying it forever (same recovery as above — Retry it from Queue once fixed, or Remove it), and won't spam Logs with the same failure on every sync.
To actually download these videos instead of skipping them, configure yt-dlp cookies file in Settings → External tools with an absolute path (inside the TunarrTube container/host) to a Netscape-format cookies.txt:
- Export cookies for
youtube.comfrom a signed-in browser session, on a machine you control, using a browser extension (e.g. "Get cookies.txt LOCALLY"). Use an account you're comfortable authenticating a background download tool as — not a primary/shared account. - Make the file available inside the container. With Docker Compose, bind-mount it read-only:
services: tunarrtube: volumes: - ./cookies.txt:/config/cookies.txt:ro
- In Settings, set the cookies file path to the in-container path (
/config/cookies.txtfor the example above), or setTUNARRTUBE_YTDLP_COOKIESbefore first start. Existing "unavailable" videos need a manual Retry — they aren't automatically retried once cookies are added.
TunarrTube never uploads, generates, or displays this file's contents — it only passes the path to yt-dlp as --cookies <path>, and that path is redacted from Logs the same way other --cookies[-from-browser] flags already are (see Security below). Treat the file itself like a password: whoever can read it can act as that YouTube account, cookies expire and need periodic re-export, and tripping YouTube's automation detection on a shared/family account risks that account, not just the download.
- Confirm at least one video is fully downloaded.
- Confirm Tunarr can read the same media directory.
- Add a path mapping if the absolute paths differ.
- Remember that
127.0.0.1inside a container refers to that container, not its host. - Run Repair video metadata if older files display their YouTube ID instead of their title.
Use Reconcile in the source's Tunarr integration panel to repair local links. Unlink only forgets TunarrTube's link; it does not delete the remote Tunarr objects.
A channel shows "Publish incomplete", or Tunarr stops responding after publishing an AI-scheduled channel
"Publish incomplete" means a Tunarr channel was created or found, but the last publish attempt didn't finish successfully — its programming/schedule write, or TunarrTube's own after-the-fact check that Tunarr can actually compute the channel's guide, failed. Click Republish/Update Tunarr Channel to try again; TunarrTube reuses what already succeeded rather than starting over. Only a channel that has actually reached a verified publish shows a plain Published badge.
This check exists because of a real Tunarr bug: a channel whose schedule ends up empty, or whose Custom Show references content Tunarr can no longer resolve, can send Tunarr's own guide-builder into an infinite loop that pegs a CPU core and makes Tunarr's entire server unresponsive for every channel — not just the one being published. TunarrTube now verifies each publish's guide before reporting success specifically to catch this as one failed publish attempt instead of a dead Tunarr instance, but it can't fix the underlying bug, which lives entirely in Tunarr itself (tracked upstream at chrisbenincasa/tunarr#2087). If Tunarr is already unresponsive, restarting it alone won't help — guide-building runs at startup, so it re-triggers the same hang. Recovery requires removing the offending channel directly from Tunarr's own database before restarting it; see the linked issue for the mechanism.
Removing a source only removes its TunarrTube catalog entry. The source's media directory is left on disk, and TunarrTube never calls the Tunarr API to delete remote objects — any Tunarr channel or Local Media source built from that directory keeps existing in Tunarr, now orphaned from TunarrTube's perspective. Delete the channel in Tunarr yourself, and remove the files from the media directory yourself, if you want them gone too.
If I delete a source that was published as a Tunarr channel, does that remove the channel or the downloaded media? No. Source deletion only removes TunarrTube's own catalog rows for that source. It preserves the source's media directory on disk and never calls the Tunarr API, so any linked Tunarr channel and Local Media source keep existing in Tunarr, unlinked from TunarrTube. Delete the channel in Tunarr and the files on disk yourself if you want them gone too.
Does TunarrTube ever delete a video because it disappeared from YouTube?
No. A sync only marks the corresponding membership missing; it never deletes the video's metadata or an already-downloaded file.
Can I run more than one TunarrTube instance against the same database? No. The background job worker and scheduler keep their state in-process with no distributed locking, so only a single running instance is supported per SQLite database.
Does TunarrTube support private, unlisted, or age-restricted videos?
Private, unlisted, and account/members-only media: no — TunarrTube can only analyze a public HTTPS URL you already have, never browse a signed-in account's library. Age-restricted or bot-checked public videos: optionally, if you configure a yt-dlp cookies file (Settings → External tools) — see "Age-restricted or sign-in-required videos" above. Without one, those videos are marked unavailable with an explanation instead of downloading.
If I change the media directory in Settings, does it move my existing downloads? No. Only future downloads use the new directory; already-completed files keep their recorded paths and are not moved.
Can a Tunarr channel stream a video directly from YouTube, without TunarrTube downloading it? No, for any playback mode. Tunarr's local-media source only scans real files on disk, and the signed YouTube CDN URL TunarrTube resolves for on-demand streaming is short-lived — it can't be handed to Tunarr once and replayed later on a channel's schedule. Publishing a Cache or Stream source to Tunarr therefore materializes (fully downloads) every video first, the same as Permanent download; the playback mode only affects retention outside of Tunarr, not whether a Tunarr channel streams live. This is a limitation of Tunarr's local-media scanner, not a TunarrTube setting — it would need Tunarr to support a remote/URL-backed media source to change.
TunarrTube's API can start downloads, change writable paths, and mutate the configured Tunarr server. The default native and Compose configurations bind to host loopback only. If remote access is required, use an authenticating reverse proxy or VPN with TLS and access controls. Anyone who can reach the API can also set the yt-dlp cookies file path (see "Age-restricted or sign-in-required videos") to any file readable inside the container, so the same access controls that protect downloads/settings also protect that YouTube account's session.
See SECURITY.md for the supported-version policy, deployment guidance, and private vulnerability-reporting process.
npm install
npm test
npm run typecheck
npm run buildUse npm run test:watch during development and npm run db:migrate after editing prisma/schema.prisma. Commit generated migrations with schema changes.
Architecture and product context live in:
- Release notes
- Product overview
- Architecture
- Engineering decisions
- Contributing guide
- Code of Conduct
If TunarrTube is useful to you, consider supporting its development:
TunarrTube is available under the MIT License.
TunarrTube is not affiliated with or endorsed by YouTube or Tunarr. You are responsible for ensuring that downloading or streaming media complies with applicable law, the source platform's terms, and the rights of content owners. The MIT license covers TunarrTube's source code, not downloaded media.

