Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 

Repository files navigation

seedbox-mover

Safely migrate completed torrents — data + .torrent + seeding duty — from a remote seedbox qBittorrent instance to a local one, using the Qui API to drive both clients. The seedbox copy is deleted only after the local copy is verified seeding. Single-file Python 3, standard library only, no pip install.

Built for the common homelab case: you keep long-term seeds on a home box (Unraid, Proxmox, a NAS) but grab on a seedbox, and want to move the seeding duty home without re-downloading over BitTorrent (which would re-report to the tracker and cost you ratio on private trackers).


Why not just re-add the torrent locally?

Because on a private tracker the downloading client reports that download to the tracker, so a plain BitTorrent re-download counts against your ratio all over again. seedbox-mover moves the bytes out-of-band with rclone and only ever hands qBittorrent an already-complete copy, so the tracker sees a seeder appear, not a leecher.

How it works (per torrent)

select (completed, in your categories, older than N days, not already handled)
  │
  ├─ already on the local client at 100% ──► recheck → tracker-host match
  │                                          → pause the seedbox copy → tag it
  │                                            "mover-verified"            (no transfer)
  │
  ├─ same *content* already local (cross-seed twin) ──► hardlink it (one physical copy, both seed)
  │
  └─ otherwise ──► pause on seedbox
                   → rclone pull the data (parallel streams)
                   → add to the local client STOPPED, correct save path/category
                   → force-recheck: must be 100% piece-hash verified (it cannot download
                     anything while stopped, so a bad transfer can never trigger a swarm
                     re-download)
                   → resume → tag the seedbox copy "mover-verified"

The tag means the data is verified — 100% piece-hash on the matching tracker host. A live working announce is deliberately not required to move (it's transient: a tracker or client blip fails it even when the torrent is fine); the announce is fired-and-reported during the move, and re-verified live at --delete, the only irreversible step.

Deletion is a separate, explicit step (--delete) that re-verifies every gate live — a fresh on-disk re-hash, tracker-host match, and a working announce — immediately before it removes the seedbox copy. Nothing is ever deleted on the strength of a stale tag.

Safety model — fail closed, everywhere

  • A deletion happens only after a live re-hash proves the local copy is 100% and a fresh announce is accepted by the tracker. A stale tag alone can never delete anything.
  • Transfers land STOPPED and are piece-hash rechecked before they're allowed to seed — a corrupt or partial pull fails the gate and is left for you, never seeded, never swarm-completed.
  • rclone --ignore-existing means a pull can never overwrite a file another local torrent is already seeding.
  • Landing collisions auto-resolve. When the same album exists on two trackers with different encodes (identical folder name, different bytes), the incoming copy lands at a unique per-infohash path so both seed; byte-identical twins are hardlinked to one physical copy instead.
  • Cross-seed twins are never orphaned. Deleting a torrent's files destroys the folder, so every other torrent seeding from it dies too. A folder is only deleted once every torrent sharing it has itself migrated and passed its own live gates; otherwise the delete is refused. Twins are adopted during the move — re-registered against the already-migrated copy, zero bytes transferred — and --delete re-derives the check from live state rather than trusting the move pass.
  • Persistent failures are quarantined (logged, skipped) so they don't silently stall the queue.
  • Exactly one copy seeds. The seedbox copy is paused before it is tagged, so the same torrent never seeds from both machines; on any failure the side left seeding is the one that actually holds verified data (local if verified-complete and active, else seedbox).
  • Single instance at a time (flock); non-idempotent API calls are never blindly retried.
  • One instance only ever deletes on the SEEDBOX side, and only its own tagged torrents.
  • Rides out transient blips. A qbit/Qui restart or brief tracker outage mid-run doesn't abort it: per-torrent failures fail closed and the run continues, GETs retry with backoff, and tracker health never gates the move — a blip can't strand a verified transfer, because the working-announce check lives at --delete, where it is re-run live every time.

Requirements

  • python3 (3.8+), standard library only — nothing to install.
  • Qui managing both qBittorrent instances (local + seedbox), reachable over HTTP with an API key.
  • rclone with a remote pointing at the seedbox's torrent storage (SFTP/FTP/etc.).
  • Run it where it can read and hardlink the local client's torrent data — usually the same host, LXC, or container as the local qBittorrent.

Setup

  1. Configure — edit the USER CONFIGURATION block at the top of seedbox-mover.py (instances, categories, paths, rclone remote — see the config reference below).
  2. API key — in Qui: Settings → API Keys → Create. Put the key (and nothing else) in a file:
    printf %s 'YOUR_QUI_API_KEY' > api.key && chmod 600 api.key
    (or export SEEDBOX_MOVER_API_KEY instead).
  3. rclone remote — e.g. rclone config create seedbox sftp host=... user=... pass=...
  4. Dry-run — run with no flags; it reports what it would do and changes nothing.

Usage

python3 seedbox-mover.py                 # dry-run: report only, touches nothing
python3 seedbox-mover.py --run           # migrate + verify up to --max torrents; tags them,
                                         # deletes nothing
python3 seedbox-mover.py --delete        # delete seedbox torrents tagged mover-verified
                                         # (every gate re-verified live first)
python3 seedbox-mover.py --delete --dry-run-delete   # preview what --delete would remove
python3 seedbox-mover.py --flagged       # read-only: list quarantined / failed / unhealthy
                                         # torrents with disposition hints
python3 seedbox-mover.py --clear-quarantine          # un-quarantine (all, or --hashes) and retry
python3 seedbox-mover.py --recover-missing           # report seedbox torrents stuck in
                                                     # missingFiles and how they'd be recovered
python3 seedbox-mover.py --recover-missing --run     # re-register them against local data

Flags: --max N caps torrents per run (move-pass default 10; --delete defaults to all tagged; --max 0 = none). --hashes H1,H2 restricts to specific infohashes.

Typical rhythm: --run → review the mover-verified tags in Qui → --delete. It's cron-friendly (schedule --run, keep --delete supervised until you trust it); only one instance runs at a time.

Cancelling a long run: send SIGTERM/SIGINT (Ctrl-C, or pkill -f seedbox-mover). It finishes the torrent it's on — completing its gates or failing closed — then exits at the next boundary, never mid-transfer. SIGKILL (-9) also stops safely but leaves one partial pull for the next run to resume.

Configuration

All settings live in the USER CONFIGURATION block at the top of the script. The important ones:

Setting What it is
QUI / API_KEY_FILE Qui base URL and the file holding your API key
LOCAL_INSTANCE / SEEDBOX_INSTANCE the two instance names exactly as shown in Qui
CATEGORY_MAP {seedbox_category: local_landing_category} — which categories to migrate and where they land (must be dedicated seed categories, not *arr's)
VERIFY_ONLY_CATEGORIES categories handled only when the same torrent already exists locally at 100% (verify → pause → tag, never transferred) — for cross-seed twin categories (e.g. {"cross-seed-link"}) that span trackers/content outside what you migrate
ADOPT_CROSS_SEED_TWINS when a torrent shares its folder with a cross-seed twin, migrate the twin with it (zero transfer — same bytes). False leaves such folders on the seedbox permanently, since they can never be safely file-deleted
RCLONE_REMOTE / REMOTE_STRIP_PREFIX rclone remote name, and the prefix stripped from a seedbox content_path to get the path relative to that remote
QBIT_DATA / HOST_DATA the torrent-data root as the local qbit sees it (container path) vs. as this script sees it (host path); equal if qbit runs natively
FUSE_ROOT / PHYS_MOUNTS for hardlinking across a union/multi-disk filesystem. Single disk/dataset: set both to the same real root (Unraid uses /mnt/user + the array disks; ZFS across datasets lists each)
CHOWN "uid:gid" to own pulled files, or "" to rely on umask
COLLISION_RESOLVE / QUARANTINE_PARTIAL_LOCAL how persistent failures and same-name collisions are handled

Setup profiles (Unraid, Proxmox/ZFS, plain Docker, single disk) are described inline in the config comments.

Notes & limitations

  • The mover and Qui's own cross-seed engine can both import the same cross-tracker twins; they cooperate (the mover detects a mid-run import and steps aside), but you may see it defer work to cross-seed.
  • If a seedbox torrent's root folder was renamed in qBittorrent so it differs from the .torrent's metainfo name, that torrent fails recheck and is quarantined (fail-closed). Fix the name on the seedbox and re-run.
  • Not affiliated with Qui/autobrr or qBittorrent. This is a community script that uses their public APIs.

License

MIT — see LICENSE.

About

Migrate completed torrents (data + .torrent + seeding duty) from a remote seedbox qBittorrent to a local one via the Qui API — verifies before deleting the seedbox copy. Python 3, stdlib only.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages