sharerr
EXPERIMENTAL UNTIL v1.0.0
Share your media library with friends, over the tools you already run.
The Scorecard badge undercounts Pinned-Dependencies: every workflow’s local
uses: $/.github/...reference is flagged as an unpinned third-party action, because Scorecard only recognises the older./form as same-repository. Tracked upstream in ossf/scorecard#5232, with a fix open in ossf/scorecard#5233.
sharerr connects to your *arr apps (Sonarr, Radarr, Lidarr, Readarr, Whisparr), finds everything tagged sharerr, builds a torrent for each file where it already sits, seeds it through your torrent client (qBittorrent, Transmission or rTorrent), and publishes the lot as a Torznab feed. Your friend adds that feed to their Prowlarr; their Sonarr and Radarr then find your releases with the TVDB/TMDb/IMDb ids attached, so a release matches a known series or film rather than being guessed from its filename.
Nothing is copied, renamed, re-linked, or moved. That is the constraint the whole design is built around.
View these docs as a website. The reference material is indexed in docs/README.md; deployment layouts are in docker/deploy/.
Contents
- What works today
- Screenshots
- Quickstart
- Sharing with a friend
- Sharing music, books, and more
- Friends finding each other
- Topology
- Sharing a plain directory, no *arr app at all
- Authenticating to qBittorrent
- Using Transmission instead of qBittorrent
- Using rTorrent / ruTorrent instead of qBittorrent
- The CLI
- Building and testing
- Layout
- Roadmap
- Getting help and contributing
- AI usage
- Licence
What works today
- Sources: discovery by tag from Sonarr, Radarr, Lidarr, Readarr and Whisparr, or a plain directory with no *arr app at all.
- Seeding: torrents built in place, files never moved; seeding through qBittorrent, Transmission or rTorrent/ruTorrent; per-torrent upload cap and seed-ratio goal; a built-in BitTorrent tracker served by sharerr itself.
- The feed: Torznab for Prowlarr, Jackett-compatible URLs and JSON results, media metadata in the release (resolution, codecs, channels, runtime; sample rate, bit depth and real format for audio).
- Friends: per-friend keys with revoke and last-seen; per-friend scoping (this friend sees TV, that one films); revoking a friend cuts tracker access too; safe rotation of the shared announce token; peer endpoint memory, signed endpoint gossip between friends, and the lighthouse for a friend whose address rotated while nobody was watching.
- Networking: a dynamic endpoint from gluetun (rotating exit IP and forwarded port), a Topology page drawing sources, this instance and friends in one picture, a live per-torrent swarm view with hourly history, and a reachability script for checking from outside your network.
- The web UI: first-run wizard, settings with connection tests, an Items page with per-item detail and manual retry/rebuild/unshare, library composition, sync history, self-refreshing status tiles, and path-mapping diagnostics.
- Operations: webhook notifications (generic, Discord, Apprise) on nine triggers plus an Uptime-Kuma-style heartbeat, config backup and restore,
/metrics(OpenMetrics) and a dashboard-widget JSON endpoint behind a bearer token, and an OpenAPI 3.1 document for the machine-facing API.
Which apps, clients and indexers are supported, how the three clients differ, and what was tried and deliberately left out is in docs/SUPPORT.md.
Screenshots
![]() | ![]() |
![]() | ![]() |
![]() | ![]() |
Settings is one long page; it is shown here in four parts, top to bottom.
![]() | ![]() |
![]() | ![]() |
Quickstart
docker run -d --name sharerr \
-p 8477:8477 \
-e SHARERR_MASTER_KEY="$(openssl rand -base64 32)" \
-v sharerr-config:/config \
-v sharerr-data:/data \
-v /path/to/library:/media:ro \
ghcr.io/ivylikethevine/sharerr-rs:latest
:latesttracks the newest tagged release. To pin a specific version, useghcr.io/ivylikethevine/sharerr-rs:vX.Y.Z, or to trackmainbetween releases,ghcr.io/…:sha-<commit>(see the tag scheme). Building it yourself withdocker build -f docker/Dockerfile -t sharerr-rs .works too.
Then open http://localhost:8477/. The first visit asks you to create an account; whoever gets there first claims the instance, so do it now rather than leaving it reachable and unclaimed. A short wizard walks through the Sonarr and Radarr URLs and API keys, the qBittorrent URL and API key, the path mappings, and the tracker’s advertised host, or skip it and use Settings, which holds the same fields and everything else. Each service has a Test connection button, and saving takes effect within a second or two, no restart.
Three things to know before going further:
SHARERR_MASTER_KEYis the one thing that cannot come from the UI, because it encrypts the vault the UI writes into. Keep it: losing it means losing every stored credential. See vault secrets.- Two volumes matter.
/dataholds the vault, the database, and the generated.torrentfiles;/configholdssharerr.toml, which the UI rewrites in place. Both must persist. Compose layouts for the common shapes are indocker/deploy/. - Port 8477 carries the web UI, the tracker, and the feed. Anyone who can reach it can reach the login page, and on a plain-HTTP LAN the session cookie travels in the clear. See the security policy for what a TLS-terminating proxy in front changes.
Sharing with a friend
sharerr publishes what it shares as a Torznab feed, which is what Prowlarr speaks. Open Friends, add your friend by name, and sharerr generates a key just for them, shown once, alongside the feed URL. They add a Generic Torznab indexer in their Prowlarr using those two values.
Because each friend has their own key, the Friends page can tell you when each of them last used the feed (“never” means they have the key but have not finished setting up), and revoking one person leaves everybody else working. That key is also what the feed embeds as the announce token, so revoking a friend cuts their access to sharerr’s tracker too, instantly, with no effect on anyone else. You can also scope what each friend sees: everything, or only TV, films, music or books. Content outside a friend’s scope is never listed and never offered, and they cannot search their way around it.
There is no shared feed key. A single
torznab.api_keywould open the feed for everybody and make revoking one friend meaningless, so asharerr.tomlcarrying a[torznab]section is rejected as an unknown key. Issue each friend their own.
If your friend has a client set up for Jackett rather than Prowlarr, it works unmodified: sharerr answers Jackett’s URL shape (/api/v2.0/indexers/<anything>/results/torznab/api) with the same feed, plus its read-only admin endpoints. Jackett’s write endpoints are not implemented; a client that calls one gets a 501 and sharerr logs the exact method and path.
Tag something before your friend adds the indexer. Sonarr and Radarr treat an empty feed as a failed test, so an indexer added before anything is shared will not validate, even though nothing is wrong.
The feed lists only what is actually seeding, and both the feed and the .torrent downloads require the key. The feed URL is built from tracker.advertised_host, so that has to be an address your friend can reach; whatever you do to make port 8477 reachable also makes the tracker and the feed reachable.
The tracker
sharerr serves /announce and /scrape from its own process, whichever torrent client seeds, and answers only for torrents sharerr made. Optionally generate an announce token under Settings → Tracker: it is embedded in the announce URL of every torrent built afterwards, so holding the .torrent is what grants the right to announce.
Rotating that token does not cut off torrents already published. The old token keeps working, unattributed, alongside the new one until you explicitly finish the rotation from Settings; the page shows whether anything has used the old token since, so you can wait until nothing has. This is a safety net for the shared token, not a substitute for per-friend revocation above.
One caveat: the announce endpoint is part of sharerr serve, so a one-shot sharerr sync produces correct torrents whose announces fail until serve is running. Field reference: [tracker].
Seeding limits
Settings → Seeding limits takes an upload-speed cap (KiB/s) and a seed-ratio goal, applied to each torrent as sharerr hands it to the client and restated on the torrents sharerr already created whenever a value changes:
[seeding]
upload_limit_kib = 500
ratio_limit = 2.0
The client’s own seeding engine honours them from then on, the same as for a torrent added by hand. A changed value reaches every torrent sharerr created on the next sync pass, once; a torrent sharerr adopted keeps whatever limits it had. A blank field is no opinion rather than “no cap”: sharerr sends nothing for it, so a limit you want gone comes off in the client. rTorrent honours the cap but not the ratio; see docs/SUPPORT.md.
The same panel also holds two settled “Before v1” roadmap questions, since an operator reasons about them together:
[seeding]
private = true # default
[feed]
magnet_links = false # default
seeding.private sets BEP 27’s private flag on torrents built from then on — on by default, which is the whole reason sharerr’s own tracker exists. Turning it off lets a client also find peers via DHT and PEX, which means revoking a friend no longer removes them from that torrent’s swarm. feed.magnet_links makes the Torznab and Jackett feeds advertise a magnet alongside the .torrent link, off by default because a magnet can never resolve against a private torrent. Turning it on only ever produces a magnet for an item that is itself not private; the combination “magnets on, everything still private” is accepted but produces nothing, rather than advertising a link guaranteed to stall a friend’s client. See docs/SUPPORT.md for why this was an open question and how it was resolved.
A dynamic endpoint (gluetun)
Behind a VPN with provider port forwarding there is no stable address to type into tracker.advertised_host. Point sharerr at gluetun’s control server instead:
[gluetun]
control_url = "http://localhost:8000" # sharerr inside gluetun's namespace
poll_secs = 60
sharerr polls gluetun for the exit IP and forwarded port, and torrents carry an announce list spanning the recently held endpoints, so a friend’s client falls back through older tiers after a rotation. When the endpoint changes, sharerr rewrites every cached .torrent (the info hash is untouched) and repoints the tracker lists inside the torrent client immediately. For reconnects to be picked up in seconds, set gluetun’s VPN_PORT_FORWARDING_UP_COMMAND to wget -qO- http://localhost:8477/gluetun/refresh and VPN_PORT_FORWARDING_DOWN_COMMAND to wget -qO- http://localhost:8477/gluetun/down; both only nudge sharerr to re-ask the control server. gluetun’s control server requires an API key (gluetun.api_key in Settings); without one sharerr skips the poll rather than send a request that can only fail.
Two related settings for constrained setups: tracker.advertised_url takes a full base URL for reverse-proxied instances, and tracker.bind opens a second listener carrying only the tracker, for the topology where exactly one forwarded port exists and it has to be the tracker’s. If the torrent client sits behind a different gluetun than sharerr does, a second poller, [gluetun_client], watches that tunnel with the same fields; that layout is docker/deploy/dual-vpn/.
Field reference and how to mint gluetun’s key: [gluetun] and deploying.
The lighthouse
Gossip only helps a friend who can still reach somebody; two friends whose addresses both rotated while neither was watching have no path back to each other. The lighthouse is the rendezvous for that case: a key hash → latest endpoint service, independent of the rest of sharerr, that a peer reports its endpoint to and a friend looks up under the key that peer issued them. A request without a valid key gets a plausible fabricated answer rather than an error, so scraping it yields only noise.
Using one is a Settings → Lighthouse field:
[lighthouse]
urls = ["https://a-friends-lighthouse.example"]
Running one, either as its own container (sharerr-lighthouse, its own image on port 7878) or embedded on one of sharerr’s own listeners, and the design behind the fabricated answers, are in docs/LIGHTHOUSE.md.
Sharing music, books, and more
Each *arr app is its own optional section, and any combination works:
[lidarr]
url = "http://localhost:8686"
[readarr]
url = "http://localhost:8787"
[whisparr]
url = "http://localhost:6969"
Then store each key: printf %s "$KEY" | sharerr vault set lidarr.api_key.
- Tags live on the artist and the author, not the album or the book, so tagging one shares their whole discography or catalogue, the same way tagging a Sonarr series shares every episode.
- Lidarr and Readarr are on API v1, Sonarr/Radarr/Whisparr on v3. sharerr picks the right one per app; you only supply the base URL.
- Whisparr content is categorised as XXX, not TV, and a friend scoped to “TV only” does not receive it. Only an unscoped friend does.
Friends finding each other
A peer is an identity, not just a credential: sharerr remembers where each friend was recently seen, with their feed traffic and their torrent client recorded separately (a dual-VPN friend has the two behind different exits). Sightings come from authenticated feed pulls, from gossip (when a friend also runs sharerr, the two instances exchange signed endpoint records over the same per-friend key the feed uses, so one friend noticing a moved address is enough for everyone who already knows them), and, when gossip has no path back to a quiet friend, from a lighthouse, ranked below both.
The trust model, stated plainly: every record is Ed25519-signed by the peer it describes, so a friend can relay it but never rewrite it; an older record never overwrites a newer one; a peer’s identity key is pinned on first use; and a gossip pull returns only records for peers the caller proves they already know.
Set it up per friend on the Friends page: their sharerr’s URL, and the key they issued you. Leave both empty and your instance still answers their pulls and accepts their pushes; it just never initiates.
A friend who stops showing up can be reported rather than noticed: with a webhook URL stored as notifications.webhook_url, sharerr POSTs there on whichever of nine triggers are enabled (a sync failing, a friend going quiet or making first contact, the advertised endpoint rotating or the tracker behind it going unreachable, items newly shared or failing to share, a library path becoming unreadable, a friend’s key being revoked), as generic JSON, a Discord webhook, or an Apprise /notify. A tenth, the heartbeat, goes the other way: a push to an Uptime-Kuma-style URL while the instance is ready, so a monitor notices the silence. Field reference: [notifications].
Topology
The Topology page is one diagram of how this instance connects to everything around it: library sources on the left, this instance and its torrent client in the middle, friends on the right. It draws nothing new; every fact on it already lives on Settings, Status, or the Friends page. A solid line to a friend means their address was seen directly, dashed means gossip relayed it, dotted means a lighthouse answered it. Under the diagram, Torrent client shows what the client is actually doing (the one place a torrent paused or removed behind sharerr’s back shows up) and Active swarms lists who is connected to each torrent right now.
Networking only (/topology?view=networking) hides the sources lane. Addresses are redacted by default so the page is safe to screenshot; a checkbox reveals them. Both choices are remembered per browser.
Checking that you are actually reachable
Two separate things, because they answer different questions. Settings → Automatic checks has an opt-in reachability probe that dials this instance’s own advertised addresses from the Topology page; a failure there says could not confirm rather than “your port is shut”, because a host dialling its own public address is exercising NAT hairpinning, which plenty of working routers refuse.
The Debug page settles it. It shows what sharerr believes its own addresses are and hands you a bash + curl script with them filled in. Run it from somewhere else (a phone off wifi, a VPS) and it reports whether the tracker and the feed are reachable from outside. Any HTTP status counts as reachable: the feed answering 401 still proves the port is open.
Sharing a plain directory, no *arr app at all
Point sharerr at a folder and everything in it is shared:
[[library]]
path = "/media/extras"
kind = "movie" # tv, movie, music, or book
[[library]]
path = "/media/tapes"
kind = "tv"
Each entry is scanned recursively; being in the directory is the tag, and kind decides the feed category and which scoped friends see it.
- No external ids travel with these releases. A friend’s app matches them by parsing the release name alone, so name files the way releases are named:
Show.Name.S01E02.mkv,Film.Title.2019.mkv. Atvfile with noSxxEyyin its name is skipped (anddoctorsays so). - Music and books lean on the directory layout:
Artist/Album/01 - Track.flacandAuthor/Title.epub. - One file, one torrent. An album is shared per track file.
- The directory is never modified, same as everywhere else in sharerr.
Authenticating to qBittorrent
sharerr signs in with a qBittorrent 5.2+ WebUI API key: stateless, no session to expire. Generate one under Options → Web UI → API key, then:
printf %s "$KEY" | sharerr vault set qbittorrent.api_key
Rotating the key in qBittorrent invalidates the old one immediately, so store the new one at the same time. Older builds without the API key feature are not supported.
If a correct key is rejected
qBittorrent validates the Host header’s port against the port it listens on, and answers 401 before it reads the key when they differ. A remapped docker port (-p 18080:8080) or a reverse proxy on another port trips this. Either point qbittorrent.url at the port qBittorrent itself listens on, or turn off Options → Web UI → Validate Host header. sharerr doctor names this, rather than reporting “rejected the API key”.
Using Transmission instead of qBittorrent
torrent_backend = "transmission"
[transmission]
url = "http://localhost:9091"
username = "transmission"
label = "sharerr" # stands in for qBittorrent's category and tag
Then store the password: printf %s "$PW" | sharerr vault set transmission.password. Transmission cannot skip the hash check, so the first add of a large library is slower; the full comparison of the three clients is in docs/SUPPORT.md.
Using rTorrent / ruTorrent instead of qBittorrent
rTorrent has no HTTP server of its own, so rtorrent.url is the exact address your reverse proxy answers XML-RPC requests on (commonly /RPC2, or ruTorrent’s /plugins/httprpc/action.php), not a base address sharerr appends a path to:
torrent_backend = "rtorrent"
[rtorrent]
url = "http://seedbox.example/RPC2"
username = "rtorrent"
label = "sharerr" # stored in rTorrent's d.custom1 field
Then store the password: printf %s "$PW" | sharerr vault set rtorrent.password. Username and password are HTTP Basic Auth for the reverse proxy in front of the RPC endpoint; if yours has none, any placeholder values work. rTorrent always verifies on add, honours the upload cap but not the ratio limit, and cannot replace a stale tracker in place; see docs/SUPPORT.md.
The CLI
The UI covers everything, but each verb has a headless equivalent, which is what a scripted deployment or a secrets manager wants. Every command takes --config <path> (or SHARERR_CONFIG) and --verbose.
| Command | What it does |
|---|---|
sharerr serve | The long-running mode: HTTP, the tracker, the feed, and the reconciliation loop. What the container runs. |
sharerr sync | One reconciliation pass, then exit. --dry-run reports what it would do without touching the client. |
sharerr doctor | Checks credentials, service reachability, the tag, and path mapping resolution, the check most likely to explain “nothing is shared”. The same checks back the web UI’s Status page. --fix creates a missing tag or category; --suggest-paths proposes [[path_map]] rules by matching tagged files against a mounted directory (--search-root, default /media), never written automatically. |
sharerr vault set <key> | Reads a secret from stdin into the encrypted vault. |
sharerr vault list | Lists which secret keys are set, without their values. |
sharerr vault remove <key> | Deletes a secret from the vault. |
sharerr openapi | Prints the OpenAPI 3.1 document for the machine-facing API (--output writes it to a file). See docs/API.md. |
sharerr preview | Serves every authenticated page with invented data on 127.0.0.1:4877 (--bind to change it), for checking the UI’s layout with no instance behind it. A development aid. |
printf %s "$SONARR_API_KEY" | docker exec -i sharerr sharerr vault set sonarr.api_key
docker exec sharerr sharerr doctor
Any setting can also come from the environment (SHARERR_QBITTORRENT__URL sets qbittorrent.url); a field pinned that way renders disabled in the UI. See environment variable overrides.
Building and testing
Rust 1.98 or newer, then the verification loop:
cargo test --workspace --all-features --locked \
&& cargo clippy --workspace --all-targets --all-features --locked -- -D warnings \
&& cargo build \
&& cargo fmt --all --check
The default suite is hermetic: no network, no containers, no database. An opt-in second tier (./scripts/run_docker_tests.sh) drives a real *arr + torrent-client stack on synthetic fixtures. docs/CONTRIBUTING.md has the lint policy, the MSRV check, and what CI runs; docs/TESTING.md has the tiers.
Layout
Twelve crates, one workspace, two binaries. The crate map, how a share moves end to end, and where state lives are in docs/ARCHITECTURE.md; the original design brief and the two premises the implementation disproved are in docs/DESIGN.md.
Roadmap
Where sharerr is going next. Nothing below is a release commitment; the ordering is a judgement about value, not a schedule. An item is removed the moment it ships. An idea that gets declined instead moves to docs/SUPPORT.md, with the reason attached.
Before v1
Operational tasks that stand between the current 0.1.x releases and a 1.0, not features:
- Rehearse one real upgrade across a migration. Twelve forward-only sqlx migrations exist, all only ever run against a fresh database.
Open work
Smallest first, by how much each item touches:
- A public lighthouse. The software is done; what is missing is a public instance for a friend group that would rather not run their own.
- Transfer accounting. The tracker resolves which friend an announce belongs to but discards the
uploaded/downloadedtotals it carries. Keeping them would unlock a per-friend “served” indicator and a real bytes-out figure on the status page. - Request flow. Discovery is one-way today. An inbound request queue with an approve step is the other half of the original idea.
Getting help and contributing
- Found a bug or want a feature? Open an issue; see
docs/SUPPORT.mdfor what is supported today and the roadmap for what is already planned or considered. - Have a question, or want to show off your setup? Start a discussion.
- Found a security issue? Do not open a public issue; see
docs/SECURITY.md. - Want to contribute a change?
docs/CONTRIBUTING.md, and the code of conduct for any project space. - Wondering who’s behind this?
docs/GOVERNANCE.md: a personal project, maintained by one person in their spare time.
AI usage
Heavily inspired by: Dictionarry/Profilarr’s AI Transparency Statement
I have used generative AI to write large parts of this project. All of the code here is my responsibility regardless: AI is a tool, not an owner of a project. I have personally understood, reviewed and approved all of the AI-generated code in this repository, and mainline releases carry the same accountability to me as anything I write and publish myself.
Licence
MIT — see LICENSE.









