Skip to content

ptheofan/putiorr

Repository files navigation

putiorr

putiorr is a put.io bridge for Sonarr, Radarr, Lidarr, Readarr, and other apps that can use a Transmission download client.

It pretends to be Transmission on the *arr side, sends magnet/torrent grabs to put.io, downloads completed put.io files to a local folder, and exposes enough queue state for completed-download handling to import media into your normal library.

The common target setup is:

put.io -> putiorr -> SSD staging folder -> *arr import -> HDD/NAS library

This keeps slow library storage out of the active download path while still letting Radarr, Sonarr, Lidarr, and similar apps move or hardlink/copy media exactly the way they normally do.

Status

Version 1 focuses on the core bridge workflow and reliability over feature breadth:

  • durable SQLite state
  • Transmission-compatible RPC endpoints
  • put.io OAuth from the web UI
  • multiple *arr profiles from one putiorr instance
  • per-profile put.io folders, download folders, and RPC paths
  • WebSocket dashboard updates
  • file-level local download progress and speed
  • safe handling of delete-local-data
  • automatic cleanup of completed prowlarr-profile transfers (removed from the list and put.io, downloaded files kept on disk)

Implemented Transmission RPC methods:

  • session-get
  • torrent-add
  • torrent-get
  • torrent-remove

This is not a full Transmission replacement. It implements the pieces needed by the *arr completed-download workflow.

Quick Start With Docker Compose

The repository includes a local stack in putiorr-compose. It starts putiorr, Radarr, Sonarr, Lidarr, and Prowlarr with ports in the 17010-17020 range.

cd putiorr-compose
cp .env.example .env
docker compose up -d --build

Open:

The compose stack is development-oriented. It builds the Dockerfile dev target, bind-mounts src/, runs pnpm run dev, and enables browser reload for the putiorr UI.

See putiorr-compose/README.md for the exact Radarr, Sonarr, Lidarr, and Prowlarr settings.

Container Images

Release images are published to GitHub Container Registry:

ghcr.io/ptheofan/putiorr

Publishing happens when a GitHub Release is published. Use a semver tag with a leading v; the workflow builds the Dockerfile production target for linux/amd64 and linux/arm64.

Release tags produce image tags like:

ghcr.io/ptheofan/putiorr:vMAJOR.MINOR.PATCH
ghcr.io/ptheofan/putiorr:MAJOR.MINOR.PATCH
ghcr.io/ptheofan/putiorr:MAJOR.MINOR
ghcr.io/ptheofan/putiorr:latest

Prereleases do not receive the latest tag.

Release Gate

package.json is the source of truth for the app version. Before publishing a release, run the guarded release script with the intended semver tag. The script updates release metadata, and the Release Gate GitHub Actions workflow can then validate main with the matching tag.

The release gate uses the semver package to compare versions. It checks that:

  • package.json contains a valid semver version.
  • the requested release tag matches package.json exactly, with a leading v.
  • package.json is not older than the latest GitHub Release.
  • the release workflow still runs the release gate before publishing images.

The release workflow also runs the release gate after a GitHub Release is published, then runs the same lint/test gates as pull requests before publishing an image.

For local or test runs where the GitHub API should be pinned, set RELEASE_GATE_LATEST_RELEASE_TAG:

RELEASE_GATE_LATEST_RELEASE_TAG=vMAJOR.MINOR.PATCH pnpm release:gate
pnpm release:gate
pnpm lint
pnpm test

Creating a Release

Use the guarded gh wrapper when an agent or maintainer should create a release from the local checkout:

pnpm release:create -- vMAJOR.MINOR.PATCH
pnpm release:create -- vMAJOR.MINOR.PATCH --yes
pnpm release:create -- vMAJOR.MINOR.PATCH --yes --publish

The script accepts a release tag, updates package.json to match it with semver, requires a clean and up-to-date main checkout, and runs:

pnpm release:gate
pnpm lint
pnpm test

If package.json changed, the script stops after the checks so the metadata can be committed and merged. Once main already contains the target version, rerun the same command. Without --yes, the script only prints the gh release create command it would run. With --yes, it creates a draft release by default. Add --publish only when the release should be published immediately and the container publishing workflow should start.

Typical NAS Layout

Most real installs should share one staging path between putiorr and each *arr app, then mount final media libraries only where the importer needs them.

Example host paths:

/volume1/docker/putiorr-config # putiorr SQLite state
/volumeSSD/putiorr             # fast SSD download/staging folder
/volumeNAS/media/movies       # final Radarr library on NAS/HDD
/volumeNAS/media/series       # final Sonarr library on NAS/HDD
/volumeNAS/media/music        # final Lidarr library on NAS/HDD

Example container paths:

putiorr: /putiorr
radarr:  /putiorr and /movies
sonarr:  /putiorr and /series
lidarr:  /putiorr and /music

The important rule is that the download path reported by putiorr must be a path Radarr/Sonarr/Lidarr can also see. If Radarr receives /putiorr/radarr, the Radarr container must have that same path mounted.

Production Compose Example

After publishing a release, use the GHCR image:

services:
  putiorr:
    image: ghcr.io/ptheofan/putiorr:latest
    container_name: putiorr
    restart: unless-stopped
    environment:
      TZ: Europe/Athens
      PUTIORR_LISTEN_HOST: 0.0.0.0
      PUTIORR_LISTEN_PORT: 9091
      PUTIORR_STATE_PATH: /data/putiorr-config/putiorr.sqlite
      PUTIORR_TARGET_DIR: /putiorr
      PUTIORR_PUBLIC_URL: https://putiorr.example.com
      PUTIORR_WORKERS: "4"
      PUTIORR_CLEANUP_REMOTE_FILES: "true"
    ports:
      - "17010:9091"
    volumes:
      - /volume1/docker/putiorr-config:/data/putiorr-config
      - /volumeSSD/putiorr:/putiorr

Radarr, Sonarr, and Lidarr need the same staging mount:

services:
  radarr:
    image: lscr.io/linuxserver/radarr:latest
    volumes:
      - /volume1/docker/radarr:/config
      - /volumeSSD/putiorr:/putiorr
      - /volumeNAS/media/movies:/movies

  sonarr:
    image: lscr.io/linuxserver/sonarr:latest
    volumes:
      - /volume1/docker/sonarr:/config
      - /volumeSSD/putiorr:/putiorr
      - /volumeNAS/media/series:/series

  lidarr:
    image: lscr.io/linuxserver/lidarr:latest
    volumes:
      - /volume1/docker/lidarr:/config
      - /volumeSSD/putiorr:/putiorr
      - /volumeNAS/media/music:/music

Connect Put.io

Open the putiorr web UI and use Connect with put.io.

The UI opens put.io's OAuth authorization page. There are two supported redirect modes.

Hosted relay mode

Hosted relay mode is the default. The Docker image bakes in putiorr's public put.io app id and GitHub Pages relay URL, so normal installs do not need OAuth environment variables.

Register this callback URL in the put.io app:

https://ptheofan.github.io/putiorr/putio-oauth-relay.html

These values are baked into the image and should not be added to normal docker-compose.yml environment blocks. Forks or custom builds can override them with Docker build arguments when they intentionally use a different put.io OAuth app or relay.

The Put.io connection dialog also has an Advanced section with editable OAuth relay URL and App Id fields. Saving there stores an instance-level override in the SQLite state database. Reset clears that override and restores the baked defaults.

Only the put.io Client ID is public and safe to bake in. Do not put the client secret or the app owner's OAuth token in GitHub Actions, GitHub Pages, or any static file. The relay is static JavaScript and only passes the returned token back to the user's putiorr instance.

Self-hosted redirect mode

If you do not use the hosted relay, put.io must redirect directly to the putiorr instance:

https://your-putiorr-host/api/oauth/callback

Set PUTIORR_PUBLIC_URL to the externally reachable putiorr base URL and add the callback URL above to your put.io OAuth app. After authorization, putiorr stores the OAuth token in its SQLite database. The token survives container restarts as long as PUTIORR_STATE_PATH is on a persistent volume.

Manual token paste is also available in the UI.

Self-hosted OAuth redirect requires your own put.io OAuth app id and an empty PUTIORR_PUTIO_OAUTH_RELAY_URL. The old default 3270 belongs to put.io's Swagger test API and will reject your self-hosted callback URL.

Configure RR Profiles

Each profile maps one *arr app to:

  • a put.io destination folder
  • a local download folder
  • a Transmission RPC endpoint path

Existing *arr clients may keep Transmission's default /transmission/rpc endpoint. On that shared endpoint, putiorr links torrent-add to the profile whose unique category appears in download-dir and verifies the matching label when the client sends one. The app User-Agent scopes later listing and removal operations to that persisted profile association. A profile-specific endpoint, when configured, remains authoritative and rejects a conflicting category.

Recommended profiles:

App put.io folder Download folder RPC endpoint
Radarr putiorr /putiorr /radarr/transmission/rpc
Sonarr putiorr /putiorr /sonarr/transmission/rpc
Lidarr putiorr /putiorr /lidarr/transmission/rpc
Readarr putiorr /putiorr /readarr/transmission/rpc

No URL Base change is required for one profile per *arr app with unique categories. URL Base is an advanced setting that can optionally select the profile-specific endpoint; this is required when multiple profiles represent the same app identity, such as two Sonarr instances.

The *arr download client category creates the final staging subfolder. For example:

Radarr category: radarr -> /putiorr/radarr
Sonarr category: sonarr -> /putiorr/sonarr
Lidarr category: lidarr -> /putiorr/lidarr

That gives each app a separate staging folder while still using one shared SSD mount. Profiles may also share the same put.io folder. If two profiles request the same put.io transfer, putiorr keeps one remote transfer and independent profile associations/local staging copies; removing one does not remove the other.

Configure Radarr

Add a Transmission download client.

If Radarr is in the same compose network as putiorr:

Name: putiorr
Host: putiorr
Port: 9091
Use SSL: off
Username: blank unless configured
Password: blank unless configured
Category: radarr
Directory: /putiorr
URL Base: /radarr/transmission

If Radarr is outside the compose network, use the host/IP and published port instead:

Host: your-nas-hostname-or-ip
Port: 17010
URL Base: /radarr/transmission

Set Radarr's movie root folder to /movies.

With completed-download handling enabled, Radarr imports from /putiorr/radarr to /movies and then removes imported files from staging according to Radarr's normal settings.

Configure Sonarr

Add a Transmission download client.

If Sonarr is in the same compose network as putiorr:

Name: putiorr
Host: putiorr
Port: 9091
Use SSL: off
Username: blank unless configured
Password: blank unless configured
Category: sonarr
Directory: /putiorr
URL Base: /sonarr/transmission

If Sonarr is outside the compose network:

Host: your-nas-hostname-or-ip
Port: 17010
URL Base: /sonarr/transmission

Set Sonarr's series root folder to /series.

With completed-download handling enabled, Sonarr imports from /putiorr/sonarr to /series and then removes imported files from staging according to Sonarr's normal settings.

Configure Lidarr

Add a Transmission download client.

If Lidarr is in the same compose network as putiorr:

Name: putiorr
Host: putiorr
Port: 9091
Use SSL: off
Username: blank unless configured
Password: blank unless configured
Category: lidarr
Directory: /putiorr
URL Base: /lidarr/transmission

If Lidarr is outside the compose network:

Host: your-nas-hostname-or-ip
Port: 17010
URL Base: /lidarr/transmission

Set Lidarr's music root folder to /music.

With completed-download handling enabled, Lidarr imports from /putiorr/lidarr to /music and then removes imported files from staging according to Lidarr's normal settings.

Configure Prowlarr

Prowlarr does not usually need a putiorr profile. It connects to Radarr/Sonarr/Lidarr, syncs indexers, and those apps send accepted grabs to putiorr through their Transmission download-client settings.

In the bundled compose stack, configure Prowlarr apps with Docker-internal URLs:

Radarr URL: http://radarr:7878
Sonarr URL: http://sonarr:8989
Lidarr URL: http://lidarr:8686

Use the API keys from each app's settings page.

Progress Model

putiorr reports combined progress as two phases:

0-50%:   put.io remote transfer
50-100%: local download from put.io to the staging folder

The web dashboard shows both put.io progress and local file progress. For multi-file releases, expand the files list to see per-file status, bytes, and local speed.

Some *arr apps only read torrent-level Transmission fields, so they may display one shared percentage for all queue items from a multi-file torrent. The putiorr dashboard is the source of truth for per-file local download progress.

Local Resume And Slow Resets

Local downloads are written as .part files and resumed with HTTP Range requests. If putiorr or the container restarts while a file is downloading, the next worker continues from the existing partial file.

Slow-speed reset is optional. Set a nonzero threshold to enable it. After the startup grace window, if a file stays below the threshold for the configured duration, putiorr closes that file download and resumes it from the partial file. Files smaller than the configured minimum size are ignored by the reset policy.

Environment

Variable Default Description
PUTIORR_LISTEN_HOST 0.0.0.0 HTTP/RPC bind host
PUTIORR_LISTEN_PORT 9091 HTTP/RPC bind port
PUTIORR_TARGET_DIR ./downloads Default local download root
PUTIORR_STATE_PATH ./data/putiorr.sqlite SQLite state database
PUTIORR_PUTIO_TOKEN unset Optional initial put.io token; UI-stored token wins after OAuth
PUTIORR_PUTIO_APP_ID 9354 Public putorr put.io OAuth app id
PUTIORR_PUBLIC_URL request host Public putiorr base URL used to build the self-hosted put.io OAuth callback
PUTIORR_PUTIO_OAUTH_RELAY_URL https://ptheofan.github.io/putiorr/putio-oauth-relay.html Hosted relay URL registered as the put.io OAuth callback; set empty for self-hosted redirect
PUTIORR_PUTIO_FOLDER putiorr Default put.io folder for the default profile
PUTIORR_DEFAULT_PROFILE_NAME Custom Name for the fallback custom profile
PUTIORR_DEFAULT_PROFILE_TYPE custom Type for the default profile
PUTIORR_DEFAULT_RPC_PATH /transmission/rpc Default Transmission RPC path
PUTIORR_PROFILES_JSON [] Optional seed profiles as JSON
PUTIORR_WORKERS 4 Concurrent local file downloads
PUTIORR_POLL_INTERVAL_MS 30000 Backend put.io polling interval, minimum 5000. The web UI receives updates over WebSocket.
PUTIORR_SLOW_SPEED_THRESHOLD_BYTES_PER_SECOND 0 Local file speed threshold. 0 disables slow-speed reset.
PUTIORR_SLOW_SPEED_DURATION_SECONDS 120 Seconds below the threshold before resetting the file connection
PUTIORR_SLOW_SPEED_GRACE_SECONDS 30 Startup grace window before slow-speed checks begin
PUTIORR_SLOW_SPEED_MIN_SIZE_BYTES 104857600 Do not slow-reset files smaller than this size
PUTIORR_CLEANUP_REMOTE_FILES true Delete put.io files/transfers after local completion
PUTIORR_RPC_USERNAME unset Optional HTTP basic auth username
PUTIORR_RPC_PASSWORD unset Optional HTTP basic auth password
PUTIORR_REFRESH_ON_RPC false Refresh put.io state during RPC torrent-get calls
PUTIORR_LIVE_RELOAD enabled outside production Dev-only browser reload hook

Development

Install dependencies:

corepack enable
pnpm install

Run locally:

cp .env.example .env
pnpm run dev

Open http://127.0.0.1:9091.

Run tests:

pnpm lint
pnpm test

Build and run the development compose stack:

cd putiorr-compose
cp .env.example .env
docker compose up -d --build

License

putiorr is licensed under the GNU Affero General Public License v3.0. See LICENSE.

About

A stable put.io bridge that exposes a Transmission-compatible RPC interface for Sonarr, Radarr, Lidarr, Readarr, Prowlarr, and the rest of the *arr family.

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors