transpondarr

module
v0.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 15, 2026 License: Apache-2.0

README

Transpondarr

An anime-focused PVR — Sonarr's job, built around anime-native tooling and metadata. It monitors anime series and films, finds releases on anime indexers, drives a download client, and organizes the results into a media library.

Status: Beta. The acquisition loop runs end-to-end and unattended — add a series or a film from AniList, and what you monitor is searched, graded against your quality profile, grabbed via qBittorrent, and hardlinked into a Plex/Jellyfin-ready library. Automation ships off by default; flip it on in Settings, or set it to notify-only first to watch what it would grab without grabbing anything. Indexing is via Torznab/Prowlarr for now.

Why not just use Sonarr?

Anime breaks Sonarr's assumptions: messy fansub filenames, absolute vs. per-season numbering, release-group/dual-audio/sub preferences, and metadata that lives on AniList/AniDB rather than TVDB.

Features

Today:

  • AniList-native metadata — add series and films from AniList search, browse a seasonal discovery chart, and see upcoming episodes and film premieres on an airing calendar keyed to Japanese broadcast times.
  • Series and films, each handled as itself — an episode is matched by number and filed under its show; a film is matched by title and release year and filed into a movies library as Placeholder Film (2019)/Placeholder Film (2019).mkv. Plex and Jellyfin want those in different places, so Transpondarr keeps them there: each library takes its own root, and a film waits in the queue rather than landing in the wrong one until the movies root is set. Format decides and episode count never does, so a one-episode OVA is a series and files with them. A film's year is what automation matches on, so it waits for one to be published rather than guessing; searching and grabbing by hand work throughout.
  • Automated acquisition — recent-feed polling grabs new releases within minutes of them appearing, and a scheduled sweep backs it up for everything that already existed. Monitoring is per title and per episode — choose at add time whether to chase a whole back catalogue or only what airs next, and unmonitor anything you don't want chased — under a global off / notify-only / on switch (off until you enable it). Notify-only rehearses the whole thing — real searches and real decisions, reported rather than grabbed. Requests can be filtered to specific indexer categories, and a poll that misses a page puts the series that aired inside the gap back at the front of the search queue.
  • A Wanted queue that says why — everything still missing across the library, and everything you hold that scores below its profile's cutoff, each with the reason it hasn't been grabbed: automation off, unmonitored, waiting its turn in the search queue, blocklisted — or the release the last pass found and declined, and why.
  • Notifications and an activity feed — Discord, generic webhook, and ntfy, with per-event toggles and a test button each; an Activity page collects the in-flight queue, the grab/import history across every title, and any download left in the client that nothing is waiting on.
  • Anime-aware quality profiles — release group is the dominant axis, then resolution/source, dual audio, and sub preferences, with a score floor and hard excludes. A profile is chosen when you add a title and can be reassigned from its page later. A per-title pinned group can also mean wait for — hold new episodes for the pinned group's release before settling for another. Opt a profile into upgrades and an episode you already have is re-grabbed while what holds it scores below the cutoff, then left alone for good.
  • Failure memory — a failed release is blocklisted with escalating expiry instead of re-grabbed forever, an environmental-fault breaker stops one bad afternoon from blocklisting the library, and everything is visible and unblockable in the UI.
  • Manual control that's never refused — search and grab any release by hand, with an episode's Search opening the release list focused on that episode; profiles inform manual actions but only gate automation.
  • Seeding-safe library import — hardlink (or copy) into Plex/Jellyfin-ready naming, without breaking the seeding torrent. Episodes file into season folders or flat, whichever your scanner prefers. Season packs import episode by episode, so a back catalogue arrives in one grab, and anything the importer can't place by itself is fixable by hand from the Activity queue. Archived payloads aren't unpacked: a RAR-set download says so and names what to extract, and extracting it in place then retrying from Fix import completes the import.
  • Self-hosted, single binary — embedded web UI, login + API key auth, REST API with an OpenAPI spec, observable background jobs, and live-editable settings — no restarts.

Planned (tracked in the milestones):

  • Post-1.0: AniList account sync (auto-monitor your Watching list), adopting a pre-existing library and noticing when it changes on disk, more indexers and download clients with per-title routing between them, and Sonarr-API compatibility for existing dashboard/mobile apps.
  • Post-1.0: first-class handling for series whose releases aren't numbered the way AniList numbers them — continuously-airing long-runners, fan re-cuts, and a per-series override for when the automatic mapping is simply wrong.

Install

Transpondarr ships as a single static binary with the web UI embedded — no separate database or frontend to run.

Docker

Pull the published multi-arch image (or build locally with docker build -t transpondarr .):

docker pull ghcr.io/matthewdias/transpondarr:latest
docker run -p 9797:9797 -v ./config:/config ghcr.io/matthewdias/transpondarr:latest

The container starts as root only to fix ownership of the mounted config dir, then drops to PUID/PGID (default 1000:1000) before serving. To skip the root phase entirely, run with --user "$(id -u):$(id -g)" — the config dir must then already exist and be writable by that user.

For a real deployment alongside qBittorrent and a media server, see Docker deployment below.

Binary

Download the archive for your platform from the releases page, extract it, and run ./transpondarrd.

To build from source instead, see CONTRIBUTING.md.

First run

The server listens on :9797. The web UI uses a login (username + password): on first run you create an admin account, or set TRANSPONDARR_AUTH_USERNAME/ _PASSWORD to bootstrap one. A separate API key guards /api/* for machine clients (dashboards, scripts, a future HA integration) via the X-Api-Key header — it's generated and persisted on first run and shown in Settings → API access (set TRANSPONDARR_API_KEY to pin one). Health check (public):

curl localhost:9797/api/v1/health

Configuration

Integrations are set through TRANSPONDARR_* environment variables or edited at runtime in the Settings UI (those DB overrides take precedence over the environment and apply live, without a restart). Unset, unconfigured integrations are simply disabled — the server still starts.

Variable Default Purpose
TRANSPONDARR_API_KEY (generated + persisted) Machine-client key for /api/* (X-Api-Key). Auto-generated and saved in the DB; set to override.
TRANSPONDARR_AUTH_USERNAME / _PASSWORD Bootstrap the initial web-UI admin account on first run (otherwise use the setup screen).
TRANSPONDARR_AUTH_REQUIRED enabled enabled (always require login) | local (skip login for local/private addresses).
TRANSPONDARR_ADDR :9797 Listen address.
TRANSPONDARR_DATA_DIR ./data SQLite DB + state (/config in Docker).
TRANSPONDARR_DB <DATA_DIR>/transpondarr.db SQLite DB file path. Override to relocate the DB independently of the data dir.
TRANSPONDARR_QBIT_URL qBittorrent WebUI root; unset ⇒ no download client.
TRANSPONDARR_QBIT_USER / _PASSWORD qBittorrent credentials.
TRANSPONDARR_QBIT_CATEGORY transpondarr Category applied to grabbed torrents.
TRANSPONDARR_STALL_TIMEOUT_HOURS 6 Hours a download may sit having transferred nothing at all before its grab is failed and the release remembered; 0 waits forever. Covers a download the client reports as stalled and one still fetching a magnet's metadata. A download with any progress is never abandoned.
TRANSPONDARR_TORZNAB_URL Torznab feed (Prowlarr/Jackett); unset ⇒ no indexer.
TRANSPONDARR_TORZNAB_APIKEY Torznab API key.
TRANSPONDARR_TORZNAB_NAME torznab Display name for the indexer.
TRANSPONDARR_TORZNAB_CATEGORIES Comma-separated Newznab category IDs sent as cat= on every search and the recent feed (anime is usually 5070); unset ⇒ no filter.
TRANSPONDARR_LIBRARY_DIR Library root episodes import into; unset ⇒ episodes do not import.
TRANSPONDARR_LIBRARY_MOVIES_DIR Library root films place into, separate from the root above; unset ⇒ a grabbed film waits in the Activity queue instead of importing.
TRANSPONDARR_LIBRARY_SERIES_LAYOUT season_folders Path shape inside the series root: season_folders | flat. Films are unaffected, and switching applies to future imports only.
TRANSPONDARR_IMPORT_MODE auto auto (hardlink, copy across filesystems) | hardlink | copy.
TRANSPONDARR_AUTOMATION_ENABLED false off | notify_only | on (bools also accepted). notify_only rehearses: it reports what automation would grab, without grabbing.
TRANSPONDARR_PIN_DELAY_HOURS 0 Hours automation waits for a series' pinned group before taking another; per-series overrides in the UI.
PUID / PGID 1000 / 1000 Docker only: the uid:gid the container drops to after fixing /config ownership on start.

Auth & reverse proxies. The local auth mode skips login only for requests from loopback/private addresses with no forwarding headers, so reverse-proxied requests (which set X-Forwarded-For) always require login — a same-host proxy can't turn local-bypass into open access. Session cookies are marked Secure automatically when the proxy sets X-Forwarded-Proto: https.

Docker deployment

For a real deployment alongside qBittorrent and a media server, use docker-compose.yml as a template. Two things matter:

  • Imports hardlink from the path qBittorrent reports. Mount your shared downloads/library volume into Transpondarr at the same path qBittorrent uses, with both on one filesystem (a hardlink can't cross filesystems). The standard single-mount layout (/data/torrents + /data/media) satisfies this. The movies root is one more directory under the same mount, not a second one.
  • Ownership. Set PUID/PGID to the UID:GID that owns your media volume — the container starts as root, fixes /config ownership, and drops to that user before serving, so hardlinks into the library land with the right ownership. Persist the /config volume (it holds the SQLite DB).

Verify a running deployment (the second call needs your API key):

curl -s http://localhost:9797/api/v1/health                      # {"status":"ok",...}
curl -s -X POST -H "X-Api-Key: <key>" http://localhost:9797/api/v1/download/test
#   {"status":"ok","client":"qbittorrent"}   (502 => qBit URL/creds wrong)

Contributing

See CONTRIBUTING.md for the stack, toolchain, build-from-source steps, and codebase layout.

License

Apache-2.0. Third-party dependency licenses and notices are reproduced in THIRD-PARTY-NOTICES.md (regenerate with make notices).

Directories

Path Synopsis
cmd
transpondarrd command
internal
config
Package config loads runtime configuration from the environment with sane defaults.
Package config loads runtime configuration from the environment with sane defaults.
core/acquire
Package acquire owns search, decide, and grab, shared by the manual HTTP routes, the scheduled sweep and the feed poll so all three drive exactly one matcher.
Package acquire owns search, decide, and grab, shared by the manual HTTP routes, the scheduled sweep and the feed poll so all three drive exactly one matcher.
core/airing
Package airing keeps per-item broadcast times in step with the metadata provider, creating the wanted items a schedule names (and the ones it skips) as it goes — for a long-runner whose episode total AniList never publishes, the schedule is the only source that knows those episodes exist.
Package airing keeps per-item broadcast times in step with the metadata provider, creating the wanted items a schedule names (and the ones it skips) as it goes — for a long-runner whose episode total AniList never publishes, the schedule is the only source that knows those episodes exist.
core/auth
Package auth implements forms-based authentication: a single admin account (username + argon2id-hashed password) and opaque server-side login sessions carried in an httpOnly cookie.
Package auth implements forms-based authentication: a single admin account (username + argon2id-hashed password) and opaque server-side login sessions carried in an httpOnly cookie.
core/blocklist
Package blocklist is the pipeline's failure memory: which release of a title already failed, so the sweep stops re-deriving the same doomed ranking (#118).
Package blocklist is the pipeline's failure memory: which release of a title already failed, so the sweep stops re-deriving the same doomed ranking (#118).
core/browse
Package browse serves seasonal discovery charts from a per-season cache.
Package browse serves seasonal discovery charts from a per-season cache.
core/catalog
Package catalog is the entry point to Transpondarr's tracked-title collection.
Package catalog is the entry point to Transpondarr's tracked-title collection.
core/clients
Package clients holds the live download/indexer/library clients behind a mutex so the settings layer can rebuild and swap them at runtime — a config change takes effect without restarting the process.
Package clients holds the live download/indexer/library clients behind a mutex so the settings layer can rebuild and swap them at runtime — a config change takes effect without restarting the process.
core/decide
Package decide is the "decide" stage of the pipeline: given a tracked Title's wanted items and a set of raw indexer releases, it works out which release satisfies which item.
Package decide is the "decide" stage of the pipeline: given a tracked Title's wanted items and a set of raw indexer releases, it works out which release satisfies which item.
core/domain
Package domain holds Transpondarr's content-type-agnostic core model.
Package domain holds Transpondarr's content-type-agnostic core model.
core/download
Package download defines the pluggable download-client interface.
Package download defines the pluggable download-client interface.
core/download/qbittorrent
Package qbittorrent implements the download.Client interface against qBittorrent's WebUI API by wrapping the maintained autobrr/go-qbittorrent client, which owns the login/CSRF/session handshake and cross-version quirks (and re-logins automatically on session expiry).
Package qbittorrent implements the download.Client interface against qBittorrent's WebUI API by wrapping the maintained autobrr/go-qbittorrent client, which owns the login/CSRF/session handshake and cross-version quirks (and re-logins automatically on session expiry).
core/importer
Package importer is the final pipeline stage: it watches the download client for grabs Transpondarr initiated (rows in the grabs table) and, once a torrent completes, hands the file to a library.Target and marks the item had.
Package importer is the final pipeline stage: it watches the download client for grabs Transpondarr initiated (rows in the grabs table) and, once a torrent completes, hands the file to a library.Target and marks the item had.
core/indexer
Package indexer defines the pluggable indexer interface.
Package indexer defines the pluggable indexer interface.
core/indexer/torznab
Package torznab implements the Indexer interface against a Torznab-compatible endpoint (e.g.
Package torznab implements the Indexer interface against a Torznab-compatible endpoint (e.g.
core/jobs
Package jobs runs named background work on a fixed interval, in memory.
Package jobs runs named background work on a fixed interval, in memory.
core/library
Package library defines the final stage of the import pipeline: the LibraryTarget interface.
Package library defines the final stage of the import pipeline: the LibraryTarget interface.
core/library/mediaserver
Package mediaserver implements the library.Target interface by placing files into a Jellyfin/Plex-friendly layout, one root per format:
Package mediaserver implements the library.Target interface by placing files into a Jellyfin/Plex-friendly layout, one root per format:
core/metadata
Package metadata defines the pluggable metadata-provider interface.
Package metadata defines the pluggable metadata-provider interface.
core/metadata/anilist
Package anilist implements metadata.Provider against AniList's public GraphQL API (https://graphql.anilist.co).
Package anilist implements metadata.Provider against AniList's public GraphQL API (https://graphql.anilist.co).
core/metadata/dbcache
Package dbcache implements metadata.Cache on top of the SQLite metadata_cache table (via the sqlc layer).
Package dbcache implements metadata.Cache on top of the SQLite metadata_cache table (via the sqlc layer).
core/notify
Package notify is the notification seam: one structured Event with typed kinds, fanned out by a Dispatcher to configured adapters (Discord, generic webhook, ntfy).
Package notify is the notification seam: one structured Event with typed kinds, fanned out by a Dispatcher to configured adapters (Discord, generic webhook, ntfy).
core/notify/discord
Package discord delivers notify events to a Discord webhook as one embed.
Package discord delivers notify events to a Discord webhook as one embed.
core/notify/ntfy
Package ntfy delivers notify events to an ntfy topic as plain-text pushes.
Package ntfy delivers notify events to an ntfy topic as plain-text pushes.
core/notify/webhook
Package webhook delivers notify events as a JSON POST users script against.
Package webhook delivers notify events as a JSON POST users script against.
core/parser
Package parser turns a raw release title into structured fields — title, season, episode number(s), release group, and the quality axes (resolution, source, subtitle type, codec, version/repack, dual-audio) — using anitogo (a Go port of Anitomy): anime filename conventions are a large heuristic problem better handled by a maintained library than by hand.
Package parser turns a raw release title into structured fields — title, season, episode number(s), release group, and the quality axes (resolution, source, subtitle type, codec, version/repack, dual-audio) — using anitogo (a Go port of Anitomy): anime filename conventions are a large heuristic problem better handled by a maintained library than by hand.
core/refresh
Package refresh grows a tracked title as its provider metadata moves: a releasing title whose episode count rises (or arrives, having been unknown at add time) gains the missing wanted items on the next pass.
Package refresh grows a tracked title as its provider metadata moves: a releasing title whose episode count rises (or arrives, having been unknown at add time) gains the missing wanted items on the next pass.
core/settings
Package settings is the runtime-configuration layer.
Package settings is the runtime-configuration layer.
coretest
Package coretest provides shared test doubles and fixtures for exercising the acquisition pipeline (search → decide → grab → import) end to end.
Package coretest provides shared test doubles and fixtures for exercising the acquisition pipeline (search → decide → grab → import) end to end.
privdrop
Package privdrop lets the container start as root just long enough to fix ownership of the data directory, then sheds privileges — the in-binary equivalent of the PUID/PGID init step in linuxserver-style images, needed because the distroless image has no shell or init to do it externally.
Package privdrop lets the container start as root just long enough to fix ownership of the data directory, then sheds privileges — the in-binary equivalent of the PUID/PGID init step in linuxserver-style images, needed because the distroless image has no shell or init to do it externally.
server
Package server wires the HTTP layer: a chi router, the Huma (OpenAPI 3.1) API, API-key auth, and the embedded single-page frontend.
Package server wires the HTTP layer: a chi router, the Huma (OpenAPI 3.1) API, API-key auth, and the embedded single-page frontend.
store
Package store owns the SQLite database: connection, embedded goose migrations, and the sqlc-generated query layer (internal/store/db).
Package store owns the SQLite database: connection, embedded goose migrations, and the sqlc-generated query layer (internal/store/db).
version
Package version exposes the build version, overridable via -ldflags at build time (see .goreleaser.yaml and the Makefile).
Package version exposes the build version, overridable via -ldflags at build time (see .goreleaser.yaml and the Makefile).
Package web embeds the built frontend (web/dist) into the binary so Transpondarr ships as a single artifact.
Package web embeds the built frontend (web/dist) into the binary so Transpondarr ships as a single artifact.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL