Automating a modern home media library means retiring manual browser searches, raw torrent clients, and chaotic folders in favor of a decoupled, containerized pipeline. Instead of managing individual files, you define quality rules and let software automatically discover, verify, download, rename, transcode, and catalog movies and shows.
[tmdbid-#####] folders & zero-copy hardlinksEvery tier is completely replaceable. If a torrent tracker goes down, Usenet takes over. If a download fails, Sonarr/Radarr tries another release. If your media server changes from Plex to Jellyfin, all your local NFO metadata and organized folders stay completely intact.
Every tool in our production stack is open-source or community-driven. Bookmark these upstream repositories for documentation, updates, and issue trackers:
In a mature homelab, never run heavy transcode/download containers directly on your storage array. Storage controllers (Synology, TrueNAS, Unraid) should focus on ZFS parity, snapshots, and disk integrity.
Why pay for Usenet if torrents are free? Because they serve two completely different purposes in a resilient library:
| Feature | Usenet (SABnzbd) | BitTorrent (qBittorrent) |
|---|---|---|
| Speed | Maxes out 1 Gbps (110+ MB/s) | Variable based on seeders |
| Privacy | Direct SSL/TLS (No VPN needed) | Requires Strict VPN Tunnel |
| Seeding | None. Download & done. | Required for healthy ratios |
| Cost | ~$102 / year | Free (plus VPN cost) |
| Best For | New releases & popular TV | Old, niche, or rare media |
To get blazing fast, zero-VPN Usenet downloads, you need two subscriptions: a Provider (gives you raw backbone NNTP access) and an Indexer (the search engine that provides NZB files to Prowlarr).
UsenetExpress — $90 / year (SSL port 563, high retention, unlimited bandwidth).NZBGeek — $12 / year (Fast VIP API access directly queried by Prowlarr).The biggest mistake homelabbers make is giving friends and family access to the Radarr and Sonarr admin panels. It takes one accidental click on "Delete all files from disk" to ruin your weekend.
SeerrNG (the next-generation, actively maintained unified evolution of Overseerr & Jellyseerr, deployed via ghcr.io/snapetech/seerrng:v3.21.4) serves as our unified media request, discovery, and user onboarding front door.
Configuration in SeerrNG's administrative console takes under five minutes:
https://authentik.puckaway.org/application/o/seerrng/, and configure claims mapping for seamless Tier 1 auto-login.http://radarr:7878), API key, default root directory (/data/media/movies), and primary quality profile (HD-1080p).http://sonarr:8989), map series root folder (/data/media/shows), and configure dedicated anime series types and tags.A core principle of the Puckanet architecture is extreme defense-in-depth. The *arr ecosystem and download clients are administrative backends—they must never be directly exposed to the open internet.
The only endpoints that should have public DNS and reverse proxy routes are hardened streaming and request portals:
media.puckaway.org (SSL + rate limiting + hardware transcode)requests.puckaway.org (SSL + Authentik OIDC Tier 1 Auto-Login)All backend administration, download queues, and indexers stay strictly isolated on your private Tailscale mesh network:
sonarr.tailnet.internal)By keeping download clients and orchestration dashboards restricted to Tailscale, you eliminate the need for router port forwarding, remove credential-stuffing bot attacks from your server logs, and can safely access all your homelab tools from your phone or laptop anywhere in the world.
Once your video pipeline is running smoothly, the exact same architectural patterns easily extend to books, audiobooks, graphic novels, and music:
Instead of fragile legacy book downloaders, we employ an automated drop-and-ingest workflow centered around the Book Dock staging volume at /data/book-dock:
/data/book-dock via automated fetchers, browser uploads, or sync hooks./data/media/books/{Author}/{Series}/{Title}) without corrupting live reading locks.BookOrbit provides a unified, distraction-free reading experience across desktop, mobile, and dedicated e-ink hardware:
These high-utility sidecars keep your library clean, automate subtitle synchronization, and handle user onboarding without manual intervention:
Nothing is more frustrating than a media server mistaking the 2022 movie Avatar for the 2005 anime series, or showing generic placeholder posters. The fix is strict naming hygiene backed by TinyMediaManager (TMM).
Always configure Radarr and Sonarr to include database IDs directly in the folder name. Media scrapers will never misidentify a title when the ID is embedded:
{Movie CleanTitle} ({Release Year}) [tmdbid-{TmdbId}]/{Movie CleanTitle} ({Release Year}) [tmdbid-{TmdbId}] - [{Quality Full}]
{Series CleanTitle} ({Release Year}) [tvdbid-{TvdbId}]/Season {season:00}/{Series CleanTitle} - S{season:00}E{episode:00} - {Episode CleanTitle} [{Quality Full}]
Rather than relying on real-time external API calls during playback, we run TinyMediaManager in the background. When Sonarr or Radarr imports a file, a custom event script triggers TMM's REST API:
movie.nfo and tvshow.nfo metadata files.#!/bin/bash
# Trigger targeted TMM scan upon download completion
API_KEY="your-tmm-api-key"
TMM_HOST="10.0.0.100"
TMM_PORT="7778"
if [ "$sonarr_eventtype" = "Download" ] || [ "$radarr_eventtype" = "Download" ]; then
TARGET_PATH="${sonarr_series_path:-$radarr_movie_path}"
PAYLOAD='[{"action":"update","scope":{"name":"path","args":["'$TARGET_PATH'"]}},{"action":"scrape","scope":{"name":"new"}}]'
curl -s -X POST -H "api-key: $API_KEY" -H "Content-Type: application/json" -d "$PAYLOAD" "http://$TMM_HOST:$TMM_PORT/api/movies"
fi
Even with strict ID tagging, chaotic real-world releases—mangled ID3 tags, generic filenames, bootleg audio rips, and broadcast airing inconsistencies—can defeat standard scrapers. We deploy three battle-tested heuristic and algorithmic defenses:
Music files downloaded from varied sources often have missing ID3 tags, wrong artist labels, or generic filenames like track01.mp3, causing Lidarr and Beets to make catastrophic false matches.
The Solution: We compute AcoustID acoustic fingerprints directly from raw audio waveforms using chromaprint (fpcalc). The acoustic hash is queried against the authoritative MusicBrainz database. Regardless of stripped tags or fabricated filenames, the track, album, and disc index are matched with 100% mathematical certainty.
When automated music pipelines fall back to YouTube / YouTube Music via LYD or yt-dlp, naive searches frequently grab live crowd bootlegs, low-bitrate rips, nightcore remixes, or acoustic user covers.
The Solution: Custom extractor arguments enforce strict search result weighting:
- Topic releases over third-party uploads.Iconic series like MythBusters suffer from notorious metadata chaos: broadcast air dates, Discovery air codes, DVD production codes, and TheTVDB/TMDB numbering all wildly disagree, resulting in wrong episode titles and scrambled files.
The Solution: Automated opening narration fingerprinting. Every episode opens with narrator Robert Lee stating: "On this episode of MythBusters... Adam and Jamie test [Myth A], while Kari, Grant, and Tory investigate [Myth B]..."
By extracting subtitle tracks or running local Whisper STT (Speech-to-Text) on the first 60 seconds of audio, our script transcribes the spoken myths and fuzzy-matches the text against official episode synopsis databases. The episode is deterministically tagged and renamed to its exact TVDB SxxExx code with 100% accuracy.
import whisper, re, json, difflib, subprocess
# 1. Transcribe opening 60s of audio via lightweight local Whisper STT
def transcribe_opener(video_path):
subprocess.run(["ffmpeg", "-ss", "0", "-t", "60", "-i", video_path,
"-vn", "-ac", "1", "-ar", "16000", "-y", "/tmp/opener.wav"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
model = whisper.load_model("tiny.en")
result = model.transcribe("/tmp/opener.wav")
return result["text"].lower()
# 2. Match spoken narration ("Adam and Jamie test...") against TVDB episode log
def resolve_episode(transcription, episode_db):
best_match, highest_score = None, 0.0
for ep in episode_db:
# Fuzzy score against known myth synopsis keywords
score = difflib.SequenceMatcher(None, transcription, ep["synopsis"].lower()).ratio()
if score > highest_score:
highest_score, best_match = score, ep
# Deterministic resolution: returns verified TVDB SxxExx
return best_match if highest_score > 0.42 else None
Raw downloaded releases often contain bloated uncompressed codecs, foreign multi-channel audio tracks you will never listen to, or broken subtitle streams. We use Tdarr with distributed worker nodes to standardize our library in H.265 (HEVC).
Before encoding a single pixel, every file passes through custom logic in the Tdarr Flow:
< 500 kbps, skip immediately (< 5ms check).mov_text and eia_608 streams. These are MP4-specific codecs that cause FFmpeg multiplexer crashes when saved into MKV containers.| Criteria | CPU (libx265) | GPU (NVENC / QSV) |
|---|---|---|
| Speed | Slow (2–6 hours / movie) | ⚡ Fast (10–25 mins / movie) |
| Power / Heat | 100% CPU usage, high thermal load | Low CPU load, ~35W GPU draw |
| Compression Efficiency | Slightly smaller at equal quality | ~5–8% larger than CPU at equal VMAF |
| Homelab Recommendation | Only for small archival batches | Gold Standard for automated bulk pipelines |
When Tdarr transcodes an existing file in-place, TinyMediaManager may refuse to update its database because it reads stale -mediainfo.xml cached files. Always configure your Tdarr post-transcode hook to delete stale .mediainfo.xml files before firing the TMM REST API call!
The single most common root cause of unexpected server transcoding in a homelab isn't video codecs or high bitrates—it is incompatible subtitle streams.
Direct Blu-ray and DVD remuxes carry raw bitmap subtitle formats (PGS / hdmv_pgs_subtitle and VobSub / dvd_subtitle). Anime releases frequently embed heavily styled ASS/SSA tracks with custom rasterized fonts and precise on-screen positioning coordinates.
Most thin clients—including smart TVs, Roku sticks, Apple TV native players, and mobile browsers—cannot render bitmap or styled vector subtitles client-side. When a user turns on subtitles, Jellyfin is forced into software video transcoding to burn the subtitles directly into the video stream. This instantly destroys direct play, spikes CPU/GPU utilization to 100%, breaks HDR tone mapping, and degrades stream clarity.
Our ingestion pipeline (via Tdarr stream cleanup and Bazarr automation) standardizes all subtitles into universal SubRip (clean UTF-8 .srt):
.en.srt, stripping HTML styling tags, color codes, and malformed encodings.-disposition:s:0 default): The primary English text subtitle track is flagged with default disposition so compatible media players select it automatically without prompting the user or triggering server-side transcode burn-in.# Strip incompatible bitmap subtitles, standardize clean English text stream as UTF-8 SRT, and set default disposition ffmpeg -i "input_movie.mkv" \ -map 0:v:0 -map 0:a \ -map 0:s:m:language:eng? \ -c:v copy -c:a copy \ -c:s srt \ -metadata:s:s:0 language=eng \ -metadata:s:s:0 title="English [SRT Normalized]" \ -disposition:s:0 default \ "output_movie_normalized.mkv" # Sidecar extraction workflow for thin-client direct streaming ffmpeg -i "input_movie.mkv" -map 0:s:m:language:eng:0 -c:s srt -y "movie_title.en.srt"
If you mount your NAS media storage via SMB/CIFS to your Docker host, high-throughput concurrent writes (e.g. SABnzbd extracting at 80 MB/s while Tdarr nodes write back transcoded files) will eventually cause transient socket reconnects.
On CIFS soft mounts, writes fail silently at 4 MiB buffer boundaries—leaving you with permanently truncated media files that freeze mid-movie.
The Fix: Always use NFSv4.1 hard mounts. NFS handles concurrent multithreaded I/O and container lockups gracefully without locking kernel threads.
When Sonarr/Radarr running as root moves a downloaded file to a Synology NFS share, it attempts a POSIX chmod 644. Synology DSM intercepts this and drops Windows ACL inheritance, making the file inaccessible to Jellyfin.
The Fix: Create a scheduled task in Synology DSM Task Scheduler running every 30 minutes:
find /volume1/media/movies -mmin -60 -exec /usr/syno/bin/synoacltool -enforce-inherit "{}" \;
find /volume1/media/shows -mmin -60 -exec /usr/syno/bin/synoacltool -enforce-inherit "{}" \;
If you map volumes as /downloads:/downloads and /movies:/movies, Docker sees them as separate file systems. When a download finishes, Radarr has to copy 40 GB across disks and delete the old one.
The Fix: Mount a single parent path (e.g. /mnt/nas-data:/data) into all containers. Structure it as /data/torrents, /data/usenet, and /data/media. This allows Radarr/Sonarr to create instant, zero-cost hardlinks—meaning zero extra disk space is used while you continue seeding!
Here is a clean, battle-hardened starter compose.yaml demonstrating the Gluetun VPN sandbox, *arr services, and single-root storage mounts:
services:
# VPN Gateway with Hardware Killswitch
gluetun:
image: qmcgaw/gluetun:latest
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
ports:
- 8080:8080 # qBittorrent WebUI (bound through VPN)
- 6881:6881 # Torrent peering
- 6881:6881/udp
environment:
- VPN_SERVICE_PROVIDER=protonvpn # or mullvad, airvpn, custom
- VPN_TYPE=wireguard
- WIREGUARD_PRIVATE_KEY=your_private_key
- WIREGUARD_ADDRESSES=10.2.0.2/32
restart: unless-stopped
# qBittorrent bound strictly into Gluetun network namespace
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: qbittorrent
network_mode: "service:gluetun"
environment:
- PUID=1000
- PGID=1000
- TZ=America/Chicago
- WEBUI_PORT=8080
volumes:
- ./data/qbittorrent:/config
- /mnt/data:/data # Root data folder for instant hardlinks
depends_on:
- gluetun
restart: unless-stopped
# SABnzbd (Usenet - Direct SSL connection, no VPN required)
sabnzbd:
image: lscr.io/linuxserver/sabnzbd:latest
container_name: sabnzbd
ports:
- 8880:8080
environment:
- PUID=1000
- PGID=1000
- TZ=America/Chicago
volumes:
- ./data/sabnzbd:/config
- /mnt/data:/data
restart: unless-stopped
# Prowlarr Indexer Proxy
prowlarr:
image: lscr.io/linuxserver/prowlarr:latest
container_name: prowlarr
ports:
- 9696:9696
environment:
- PUID=1000
- PGID=1000
- TZ=America/Chicago
volumes:
- ./data/prowlarr:/config
restart: unless-stopped
# Radarr (Movies)
radarr:
image: lscr.io/linuxserver/radarr:latest
container_name: radarr
ports:
- 7878:7878
environment:
- PUID=1000
- PGID=1000
- TZ=America/Chicago
volumes:
- ./data/radarr:/config
- /mnt/data:/data
restart: unless-stopped
# Sonarr (TV Shows)
sonarr:
image: lscr.io/linuxserver/sonarr:latest
container_name: sonarr
ports:
- 8989:8989
environment:
- PUID=1000
- PGID=1000
- TZ=America/Chicago
volumes:
- ./data/sonarr:/config
- /mnt/data:/data
restart: unless-stopped
# SeerrNG Request Portal (Unified Media Discovery & Authentik OIDC)
seerrng:
image: ghcr.io/snapetech/seerrng:v3.21.4
container_name: seerrng
ports:
- 5055:5055
environment:
- LOG_LEVEL=debug
- TZ=America/Chicago
volumes:
- ./data/seerrng:/app/config
restart: unless-stopped
# Tdarr Transcoding Server + Hardware Node
tdarr:
image: ghcr.io/haveagitgat/tdarr:latest
container_name: tdarr
restart: unless-stopped
ports:
- 8265:8265 # WebUI
- 8266:8266 # Node registration port
environment:
- PUID=1000
- PGID=1000
- TZ=America/Chicago
- serverIP=0.0.0.0
- serverPort=8266
- webUIPort=8265
- internalNode=true
- nodeName=TranscodeNode
- NVIDIA_DRIVER_CAPABILITIES=compute,video,utility
- NVIDIA_VISIBLE_DEVICES=all
volumes:
- ./data/tdarr/server:/app/server
- ./data/tdarr/configs:/app/configs
- ./data/tdarr/logs:/app/logs
- /mnt/data/media/movies:/media/movies
- /mnt/data/media/shows:/media/shows
- /tmp/tdarr-scratch:/temp # Fast local scratch disk
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]