# ADR 016: Automated media acquisition

- HTML version: https://robbiepalmer.me/projects/homelab/adrs/016-media-automation-arr-stack
- Project: Home Lab (https://robbiepalmer.me/projects/homelab.md)
- Status: Accepted
- Date: 2026-08-23

# Context

[Jellyfin](/projects/homelab/adrs/011-jellyfin) solves playback, but not
supply. Each new episode needs the same mechanical treatment: locate a
release, download it, rename it to something Jellyfin parses, file it into
the right season folder, rescan the library. That pipeline never varies,
which makes it software's job rather than a recurring chore.

The hub already runs Docker under colima with launchd agents for
self-healing, so whatever I add should follow the same shape: declared in
`homelab/`, bootstrapped with mise, kept alive by launchd, reachable only on
the LAN and tailnet.

# Decision

Run the standard \*arr acquisition stack on the Mac mini, in one Compose
project (`homelab/hosts/mac-mini/media-automation/`):

* **qBittorrent** (containerized) downloads torrents into
  `/Volumes/Expansion/Downloads`. See [ADR 018](/projects/homelab/adrs/018-single-containerized-torrent-client)
  for why it is the single shared client.
* **Sonarr** watches TV series and **Radarr** watches movies. When an episode
  or film becomes available they search the indexers, pick a release, send it
  to qBittorrent, and import the finished download straight into Jellyfin's
  library folders (`/media/TV`, `/media/Movies`) renamed to Jellyfin's
  naming scheme.
* **[Prowlarr](/projects/homelab/adrs/017-prowlarr-indexer-management)**
  manages the torrent indexers once and syncs them to both apps.
* **[Recyclarr](/projects/homelab/adrs/019-recyclarr-trash-guides)** keeps
  both apps' quality profiles and custom formats synced to the TRaSH Guides
  nightly, so release picking stays sensible without hand-tuning.

The flow looks like this:

```mermaid
flowchart LR
Trackers["Public trackers"]
Guides["TRaSH Guides"]

subgraph stack["Media automation"]
Prowlarr["Prowlarr<br/>Indexer manager"]
Sonarr["Sonarr<br/>TV"]
Radarr["Radarr<br/>Movies"]
QB["qBittorrent"]
Recyclarr["Recyclarr<br/>Profile sync"]
end

subgraph drive["Media volume"]
Downloads["downloads/"]
TV["TV/"]
Movies["Movies/"]
end

Jellyfin["Jellyfin"]
Clients["Fire TV and tailnet devices"]

Trackers -->|"searches"| Prowlarr
Prowlarr -->|"syncs indexers"| Sonarr
Prowlarr -->|"syncs indexers"| Radarr
Sonarr -->|"grabs"| QB
Radarr -->|"grabs"| QB
QB -->|"saves to"| Downloads
Downloads -->|"Sonarr imports and renames"| TV
Downloads -->|"Radarr imports and renames"| Movies
TV -->|"scans"| Jellyfin
Movies -->|"scans"| Jellyfin
Jellyfin -->|"streams"| Clients
Guides -->|"nightly profile sync"| Recyclarr

classDef external fill:#475569,color:#fff,stroke:#334155
classDef service fill:#0f766e,color:#fff,stroke:#115e59
classDef storage fill:#b45309,color:#fff,stroke:#92400e
class Trackers,Guides,Clients external
class Prowlarr,Sonarr,Radarr,QB,Recyclarr,Jellyfin service
class Downloads,TV,Movies storage
```

Wiring between the services happens through their REST APIs, driven by
an idempotent `provision.py` script rather than hand-clicking in five web
UIs. Downloads carry per-app categories (`tv-sonarr`, `radarr`) so each app
only ever sees its own torrents. Downloads sit on the same physical volume as
the library, so imports are fast moves instead of cross-disk copies.

A launchd agent (`homelab.media`) re-checks every minute and brings up colima
and the Compose stack if either died, same as Jellyfin's agent. The two
layers overlap on purpose: `restart: unless-stopped` covers container
crashes while the VM runs, and launchd covers VM death and hub reboots.

# Alternatives

## Keep grabbing files by hand

* **Pros**: No new services, no indexers to maintain.
* **Cons**: The rename-move-rescan loop is weekly toil, and it silently stops
  whenever I'm busy, so the library goes stale.
* **Decision**: Rejected. This ADR exists because I already did this for a
  whole season.

## All-in-one bundle images (DockSTARTer-style)

* **Pros**: One container promises the whole stack pre-wired.
* **Cons**: Opaque internals, upgrade risk concentrated in one image, and
  debugging means learning the bundle's abstractions on top of the four apps'
  own ones.
* **Decision**: Rejected. Four plain linuxserver.io containers with explicit
  wiring stay debuggable when one of them breaks at 2am.

## Native installs on macOS

* **Pros**: No VM layer.
* **Decision**: Rejected. Everything else on the hub already runs under
  colima with the same launchd pattern; a second supervision story for four
  long-running processes buys nothing.

# Consequences

### Pros

* Subscribing to a show once means episodes appear in Jellyfin on their own.
* The whole stack is reproducible from the repo: `.env.example`,
  `bootstrap.sh`, idempotent provisioning, mise tasks (`//homelab:media-*`).
* One place to see indexer health (Prowlarr) and one queue to watch
  (qBittorrent).

### Cons

* Five more long-running containers on a 4GB VM alongside Jellyfin,
  SilverBullet, and the photo backup. Memory pressure is the first thing to
  watch.
* Public-tracker reachability from a .NET HTTP stack is fragile: some
  indexer CDNs reject Prowlarr's TLS handshake outright, so those definitions
  stay out of the provisioned set.
* The Netdata alerting ([ADR 009](/projects/homelab/adrs/009-netdata)) should
  cover these containers too; today nothing pages if Sonarr dies.

---

Markdown index of this site: https://robbiepalmer.me/llms.txt
