6adfd77236
- Default MOVE_MODE changed to copy so qBit can continue seeding - Add QBIT_CATEGORY env var (default "Stash") to filter torrents - Split compose into dev (build from source) and prod (pre-built image) - Fix Dockerfile COPY glob needing trailing slash - Update README and summary docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4.4 KiB
4.4 KiB
StashHandler — Project Summary
NOTE: Update this file after any significant changes to the project (new features, architecture changes, new env vars, schema changes, etc.)
Purpose
StashHandler sits between qBittorrent and Stash (both running in Docker). It polls qBit's API for completed torrents filtered by category, hashes media files, deduplicates them via a SQLite database, and copies new files into Stash's import directory (preserving originals for continued seeding).
Problems solved:
- Deleted files in Stash don't get reimported (hash DB remembers them)
- Duplicate content is caught before reaching Stash
- Existing library files can be recorded for dedup without re-processing
Architecture
qBit (Docker) StashHandler (Docker) Stash (Docker)
/downloads/ ──bind──> /staging (read-only)
polls qBit API
hashes files, checks SQLite
/import (read-write) ──bind──> /data/import
/data/seen.db (persistent)
Designed for Docker Swarm — no host-local config files, everything via env vars.
Files
| File | Purpose |
|---|---|
stash_handler.py |
Entry point — daemon and scan CLI commands, config loading from env vars |
db.py |
SQLite operations — init_db, is_known, record_file, is_torrent_processed, record_torrent, increment_seen |
hasher.py |
Streaming file hashing (sha256 default, blake3 optional) in 64KB chunks |
qbit_client.py |
qBittorrent API client — login, session cookies, auto re-auth on 403 |
Dockerfile |
python:3.12-slim, entrypoint is stash_handler.py, default command daemon |
docker-compose.yaml |
Dev compose — builds from source, includes qbittorrent + stash, DEBUG logging |
docker-compose.prod.example.yaml |
Prod compose template — pulls from git.fabledsword.com/bvandeusen/stashhandler:latest, stash-handler only |
requirements.txt |
requests (only dependency) |
Modes of Operation
Daemon mode (default)
docker run stash-handler
# or: python stash_handler.py daemon
- Polls qBit API at
POLL_INTERVALseconds - Fetches completed torrents via
/api/v2/torrents/info?filter=completed&category=<QBIT_CATEGORY> - Skips already-processed torrents (by info_hash in
processed_torrentstable) - For each new torrent: walks media files, hashes, deduplicates, transfers new files to import dir
- Clean shutdown on SIGINT/SIGTERM (1-second sleep granularity)
Scan mode
docker exec stash-handler python stash_handler.py scan [--import] [path]
scan /staging— record hashes only (bootstrap dedup DB, no files moved)scan --import /staging— hash and move new files to import dirscan /stash-library— record existing Stash library hashes (mount read-only)- Prints summary: total scanned, new, already known
Environment Variables
| Variable | Default | Description |
|---|---|---|
QBIT_URL |
http://qbittorrent:8080 |
qBittorrent Web UI URL |
QBIT_USERNAME |
admin |
qBit login username |
QBIT_PASSWORD |
adminadmin |
qBit login password |
STAGING_DIR |
/staging |
Mount point for qBit's download directory |
IMPORT_DIR |
/import |
Mount point for Stash's import directory |
DB_PATH |
/data/seen.db |
Path to SQLite database |
POLL_INTERVAL |
60 |
Seconds between qBit API polls |
MEDIA_EXTENSIONS |
.mp4,.mkv,.avi,.wmv,.mov,.webm,.flv,.m4v |
Comma-separated list of media file extensions |
HASH_ALGORITHM |
sha256 |
Hash algorithm (sha256 or blake3) |
MOVE_MODE |
copy |
File transfer mode: copy, hardlink, or move |
QBIT_CATEGORY |
Stash |
qBit category to filter torrents by (shared qBit instance support) |
LOG_LEVEL |
INFO |
Python logging level |
Database Schema
SQLite at DB_PATH, WAL mode enabled.
CREATE TABLE seen_files (
hash TEXT PRIMARY KEY,
filename TEXT NOT NULL,
source TEXT, -- "torrent", "scan", "bulk"
info_hash TEXT,
first_seen TEXT NOT NULL,
times_seen INTEGER DEFAULT 1
);
CREATE TABLE processed_torrents (
info_hash TEXT PRIMARY KEY,
torrent_name TEXT,
processed_at TEXT NOT NULL
);
Volume Layout (Docker)
volumes:
- /data/staging:/staging:ro # qBit's download dir (read-only)
- /data/stash-import:/import # Stash's import dir (read-write)
- /data/stash-handler:/data # persistent DB