Metadata-Version: 2.5
Name: collapsarr
Version: 1.0.0
Summary: An *arr-family companion app that adds downmixed (stereo/2.1/5.1) audio tracks to media missing them, via FFmpeg.
Project-URL: Homepage, https://github.com/JovinJovinsson/Collapsarr
Project-URL: Repository, https://github.com/JovinJovinsson/Collapsarr
Author: Jovin Jovinsson
License-Expression: GPL-3.0-or-later
License-File: LICENSE
Keywords: arr,audio,downmix,ffmpeg,radarr,sonarr
Classifier: Framework :: FastAPI
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Multimedia :: Sound/Audio
Requires-Python: >=3.12
Requires-Dist: alembic>=1.13
Requires-Dist: fastapi>=0.110
Requires-Dist: httpx>=0.27
Requires-Dist: itsdangerous>=2.0
Requires-Dist: platformdirs>=4.0
Requires-Dist: pydantic-settings>=2.2
Requires-Dist: pydantic>=2.6
Requires-Dist: sqlalchemy>=2.0
Requires-Dist: uvicorn[standard]>=0.29
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Description-Content-Type: text/markdown

<img src="frontend/public/favicon.svg" width="72" align="right" alt="Collapsarr logo" />

# Collapsarr

**Never get stuck without a downmix again.**

[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-8B5CF6.svg)](LICENSE)
[![CI](https://github.com/JovinJovinsson/Collapsarr/actions/workflows/release.yml/badge.svg)](https://github.com/JovinJovinsson/Collapsarr/actions/workflows/release.yml)
[![Docker](https://img.shields.io/docker/v/odxnsson/collapsarr?label=docker)](https://hub.docker.com/r/odxnsson/collapsarr)
[![PyPI](https://img.shields.io/pypi/v/collapsarr)](https://pypi.org/project/collapsarr/)
[![Release](https://img.shields.io/github/v/release/JovinJovinsson/Collapsarr)](https://github.com/JovinJovinsson/Collapsarr/releases)

Collapsarr is a companion application for Sonarr and Radarr. It watches your
library for media missing a lower-channel-count audio track — a 7.1 release
with no stereo fallback, a 5.1 file your soundbar can't decode — and adds one
automatically via FFmpeg, without touching the track that's already there.

## Why

- **No upmixing, ever.** Collapsarr only adds tracks the source can actually
  support (a stereo/2.1/5.1 downmix from a higher channel count) — it will
  never fake a 5.1 track out of a stereo source.
- **Originals are never at risk.** The remux writes to a temp file, validates
  duration and stream count, then atomically swaps it in. Any failure at any
  stage leaves the original file completely untouched — no partial writes, no
  orphaned backups.
- **Fits into the \*arr stack you already run.** Sonarr/Radarr integration
  (webhooks + periodic scan), a dark UI in the same style as the rest of the
  family, and a REST API following the same conventions.

## Features

- Sonarr and Radarr integration — instance config, connectivity check, remote
  path mapping, multiple concurrent instances
- Per-target, per-language detection that stacks additional targets without
  duplicating what's already there
- FFmpeg remux: stream-copies existing tracks, encodes new audio (AAC for
  Stereo, AC3 @ 448kbps for 2.1/5.1)
- Job queue with configurable concurrency — triggered by webhook, periodic
  full-library scan, on-demand scan, or manual per-file trigger
- Full job history: status, timestamps, FFmpeg exit code, error text
- Web UI in the same dark theme as Sonarr/Radarr/Bazarr — Wanted view,
  Activity/History, per-file detail view with a manual trigger
- Webhook + Discord notifications on job failure or app health issues (e.g.
  FFmpeg missing)
- REST API with \*arr-convention auth (API key)

## Quick start

**Docker (recommended):**

```yaml
services:
  collapsarr:
    image: odxnsson/collapsarr:latest
    container_name: collapsarr
    ports:
      - "8282:8282"
    volumes:
      - ./config:/config
      - /path/to/media:/media
    environment:
      - PUID=1000
      - PGID=1000
    restart: unless-stopped
```

Then open `http://localhost:8282` — you'll land on a one-time credential setup
page (see [Authentication](#authentication) for how login is enforced,
including the caveat if you're putting Collapsarr behind a reverse proxy).
`restart: unless-stopped` above means the container comes back automatically
whenever it stops unexpectedly or the Docker daemon restarts (e.g. after a
host reboot) — see [Running on startup](#running-on-startup) if you need it
to survive a reboot on a bare-metal/PyPI install instead.

**PyPI (bare-metal):**

```bash
pipx install collapsarr
collapsarr
```

No flags, no config file needed — Collapsarr stores its SQLite database
under your platform's standard per-user data directory by default (e.g.
`~/.local/share/collapsarr/collapsarr.db` on Linux; native per-OS locations
on macOS/Windows), creating it automatically if it doesn't exist. Set
`COLLAPSARR_DATA_DIR` if you'd rather it live somewhere else — see
[Configuration](#configuration). Requires FFmpeg on `PATH` — see
[Requirements](#requirements) below. Open `http://localhost:8282`; see
[Configuration](#configuration) for the full list of environment variables,
and [Running on startup](#running-on-startup) for a systemd unit.

## Requirements

- Python 3.12+
- FFmpeg — external system dependency, checked at startup and reported on the
  health page if missing. Bundled in the Docker image; install it yourself
  for a bare-metal/PyPI setup:

  | OS | Command |
  | --- | --- |
  | Debian / Ubuntu | `sudo apt install ffmpeg` |
  | Fedora | `sudo dnf install ffmpeg` |
  | Arch | `sudo pacman -S ffmpeg` |
  | macOS (Homebrew) | `brew install ffmpeg` |
  | Windows (winget) | `winget install ffmpeg` |

  Verify with `ffmpeg -version`. Official builds/source: [ffmpeg.org/download.html](https://ffmpeg.org/download.html).

## Authentication

Collapsarr requires a one-time credential setup (`/setup`, first run) and,
after that, logging in (`/login`) before the UI/API is usable — *except* from
a caller Collapsarr considers "local". The **Login requirement** setting
(Settings → General, `auth_required` in the API) controls this:

| Mode | Behaviour |
| --- | --- |
| **Disabled for local addresses** (`local_bypass`, default) | A caller connecting from a loopback (`127.0.0.1`/`::1`) or private-range (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, etc.) address reaches the UI and API with no setup and no login. Anyone connecting from a routable/public address still has to authenticate normally. |
| **Always required** (`enabled`) | Every caller is challenged, regardless of address. |

### Reverse-proxy configuration

If Collapsarr sits behind a reverse proxy (nginx, Traefik, Cloudflare Tunnel,
etc.), by default every request's direct peer is the proxy itself — meaning
requests from any real client are classified by their network distance from
the proxy, not the client. This breaks `local_bypass` mode (the default Login
requirement), where local clients should skip authentication: the proxy's own
address is usually private/loopback, so **every** client (including public
internet) appears local and bypasses login. It also corrupts the session
cookie's `Secure` flag, which should reflect the real client's connection
scheme, not Collapsarr's local connection to the proxy.

**Configure trusted proxies to fix this:** Set `COLLAPSARR_TRUSTED_PROXIES` to
a comma-separated list of IP addresses or CIDR blocks identifying your
reverse proxy(ies), e.g. `COLLAPSARR_TRUSTED_PROXIES=192.168.1.100` or
`COLLAPSARR_TRUSTED_PROXIES=10.0.0.0/8,192.168.1.100`. Once configured,
Collapsarr trusts that proxy's `X-Forwarded-For` and `X-Forwarded-Proto`
headers to classify the real client address and scheme, fixing both issues
above. An unparseable entry fails fast at startup.

**Single-hop trust only:** when the direct peer is on the allowlist, the
*rightmost* `X-Forwarded-For` and `X-Forwarded-Proto` entries (the trusted
proxy's own view of its immediate client) are used. There is no support for
multi-hop proxy chains; an install behind multiple reverse proxies must
normalize those headers before they reach Collapsarr.

**If you have not configured a trusted proxy**, the old workaround still
applies: use Settings → General (or the API endpoint `PUT /api/settings`)
to set the Login requirement to "Always required" (`auth_required: "enabled"`)
to force authentication regardless of the apparent client address. This
runtime setting persists across restarts and applies to all subsequent
requests.

**Headless deploys — seeding a credential without the setup page:** a
declarative/automated deploy (Docker Compose, Ansible, etc.) has no human
available to click through `/setup`. Set `COLLAPSARR_AUTH_USERNAME` and
`COLLAPSARR_AUTH_PASSWORD` (together — see
[Configuration](#configuration)) and a fresh install seeds that credential on
first boot instead — hashed before it's persisted, never stored or logged in
plaintext — and comes up already past the setup gate. `COLLAPSARR_AUTH_METHOD`
and `COLLAPSARR_AUTH_REQUIRED` are honoured at the same time if set, otherwise
the seeded credential gets the same defaults `/setup` would (`forms`,
`local_bypass`).

This doubles as the supported password-recovery/lockout escape hatch, but
**only for a fresh or already-locked-out install with no credential
configured yet** — seeding runs once and never overwrites a credential that
already exists, even if the environment variables are still set on a later
boot. It does **not** help recover a *forgotten* password once a credential
is already set; that requires clearing the existing `auth_username`/
`auth_password_hash` first (e.g. directly in the database) so the instance
has no credential again, at which point re-seeding (or `/setup`) applies.

**Mounting Collapsarr at a subpath (`COLLAPSARR_URL_BASE`):** if you're
routing several apps through one reverse proxy and want Collapsarr to live
under a subpath (e.g. `https://example.com/collapsarr/`) instead of its own
(sub)domain, set `COLLAPSARR_URL_BASE=/collapsarr`. Collapsarr expects the
proxy to forward the request **exactly as received — no path-rewrite/strip
rule needed**: it recognizes and strips its own configured prefix internally
(the same pattern ASP.NET Core's `UsePathBase` implements), so a plain
pass-through proxy config "just works". This is a worked example combining
it with the trusted-proxies configuration above:

```nginx
# /etc/nginx/conf.d/collapsarr.conf
server {
    listen 443 ssl;
    server_name example.com;

    location /collapsarr/ {
        proxy_pass http://127.0.0.1:8282;   # no path after host:port --
                                             # nginx forwards the full
                                             # /collapsarr/... URI unchanged,
                                             # no rewrite rule needed
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
```

```bash
# .env, or the compose `environment:` block
COLLAPSARR_URL_BASE=/collapsarr
COLLAPSARR_TRUSTED_PROXIES=127.0.0.1   # nginx's address -- see above
```

With this configuration:

- A browser request to `https://example.com/collapsarr/wanted` arrives at
  Collapsarr as `/collapsarr/wanted` (nginx forwarded it unmodified);
  Collapsarr strips the `/collapsarr` prefix internally and routes it as
  `/wanted`, exactly like an unprefixed install.
- A request that bypasses the proxy entirely — e.g. a Docker healthcheck
  hitting `http://localhost:8282/health` directly — still resolves: only
  paths that actually start with the configured prefix are stripped;
  anything else passes through unchanged.
- Redirects (to `/setup`, `/login`) and the session cookie's `path`
  attribute are both re-prefixed with `/collapsarr`, so the browser is sent
  to — and the cookie is scoped to — `/collapsarr/login`, never the bare
  `/login` (which the proxy isn't even routing to Collapsarr).
- The served page picks up the configured prefix at runtime and prefixes its
  own API calls and client-side navigation, so the UI behaves the same as an
  unprefixed install — links, redirects, and API requests all resolve under
  `/collapsarr`.

A value missing its leading slash fails fast at startup; a trailing slash is
stripped automatically. See [Configuration](#configuration) and
`docs/adr/0004-url-base-strip-middleware-not-root-path-flag.md` for the
design rationale.

## Development

```bash
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

# Run the checks
pytest
ruff check .
mypy

# Run the server (defaults to http://0.0.0.0:8282)
python -m collapsarr
# then: curl http://localhost:8282/health
#   ->  {"status":"ok","version":"...","warnings":[]}
#   (or, if FFmpeg is missing: {"status":"degraded","version":"...",
#    "warnings":[{"code":"ffmpeg_missing","message":"...","severity":"error"}]})
```

## Configuration

All settings load from environment variables (prefixed `COLLAPSARR_`) with
sensible defaults, and an optional `.env` file is read from the working
directory. See [`.env.example`](.env.example).

| Variable | Default | Description |
| --- | --- | --- |
| `COLLAPSARR_DATA_DIR` | *(OS user-data dir)* | Root directory for application data — the SQLite database today, logs/backups later. Defaults to `platformdirs.user_data_dir("collapsarr")` (e.g. `~/.local/share/collapsarr` on Linux). Created automatically if missing. The Docker image sets this to `/config` (its mounted volume) — see [Quick start](#quick-start). |
| `COLLAPSARR_DATABASE_PATH` | *(derived from `COLLAPSARR_DATA_DIR`)* | SQLite database file path. Set this to override the location directly, independent of `COLLAPSARR_DATA_DIR`. |
| `COLLAPSARR_DATABASE_URL` | *(derived from path)* | Full SQLAlchemy URL override — takes precedence over both of the above. |
| `COLLAPSARR_HOST` | `0.0.0.0` | API server bind address. |
| `COLLAPSARR_PORT` | `8282` | API server bind port. |
| `COLLAPSARR_LOG_LEVEL` | `INFO` | Log level. |
| `COLLAPSARR_TRUSTED_PROXIES` | *(empty)* | Comma-separated list of IP addresses and/or CIDR blocks (e.g. `192.168.1.100,10.0.0.0/8`) identifying reverse proxies to trust for `X-Forwarded-For` and `X-Forwarded-Proto` headers. When the direct TCP peer is in this allowlist, Collapsarr uses those headers to determine the real client address and request scheme. See [Reverse-proxy configuration](#reverse-proxy-configuration). An unparseable entry fails fast at startup. |
| `COLLAPSARR_URL_BASE` | *(empty)* | Reverse-proxy subpath prefix (e.g. `/collapsarr`) for mounting Collapsarr under a subpath instead of its own (sub)domain. Empty by default — no prefix, routes serve at the root as today. The reverse proxy needs no rewrite rule; Collapsarr strips its own configured prefix internally. See [Reverse-proxy configuration](#reverse-proxy-configuration). A value missing its leading slash fails fast at startup; a trailing slash is stripped automatically. |
| `COLLAPSARR_AUTH_USERNAME` | *(unset)* | First-boot credential seed: UI username. Set together with `COLLAPSARR_AUTH_PASSWORD` — see [Authentication](#authentication). |
| `COLLAPSARR_AUTH_PASSWORD` | *(unset)* | First-boot credential seed: UI password. Hashed before being persisted; never stored or logged in plaintext. |
| `COLLAPSARR_AUTH_METHOD` | *(unset — `forms`)* | Optional, only applied when the seed credential above is actually seeded: `forms` or `basic`. |
| `COLLAPSARR_AUTH_REQUIRED` | *(unset — `local_bypass`)* | Optional, only applied when the seed credential above is actually seeded: `enabled` or `local_bypass`. |

## Running on startup

**Docker:** the `restart: unless-stopped` line in the [Quick start](#quick-start)
compose file already handles this — Docker restarts the container whenever it
stops unexpectedly or the Docker daemon itself restarts. On Linux this
happens automatically on boot, since `dockerd` runs as a systemd service
enabled by default (`systemctl is-enabled docker` to confirm). On
Docker Desktop (macOS/Windows), enable **Settings → General → Start Docker
Desktop when you log in** so the daemon — and in turn the container — comes
up after a reboot.

**Bare-metal / PyPI install:** run Collapsarr as a systemd service so it
starts on boot and restarts if it crashes. Create
`/etc/systemd/system/collapsarr.service`:

```ini
[Unit]
Description=Collapsarr
After=network.target

[Service]
Type=simple
User=collapsarr
Group=collapsarr
WorkingDirectory=/opt/collapsarr
EnvironmentFile=/opt/collapsarr/.env
ExecStart=/opt/collapsarr/.venv/bin/collapsarr
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
```

Adjust `User`/`Group`, `WorkingDirectory`, and the `.venv` path to match
where you installed it; `EnvironmentFile` should point at an `.env`
containing the `COLLAPSARR_*` variables from [Configuration](#configuration)
(see [`.env.example`](.env.example)). Then enable and start it:

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now collapsarr
```

Check status/logs with `systemctl status collapsarr` and `journalctl -u collapsarr -f`.

## Docs

Fuller docs live on the [GitHub Wiki](https://github.com/JovinJovinsson/Collapsarr/wiki).

## License

[GPLv3](LICENSE).
