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>
108 lines
4.4 KiB
Markdown
108 lines
4.4 KiB
Markdown
# 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:**
|
|
1. Deleted files in Stash don't get reimported (hash DB remembers them)
|
|
2. Duplicate content is caught before reaching Stash
|
|
3. 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_INTERVAL` seconds
|
|
- Fetches completed torrents via `/api/v2/torrents/info?filter=completed&category=<QBIT_CATEGORY>`
|
|
- Skips already-processed torrents (by info_hash in `processed_torrents` table)
|
|
- 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 dir
|
|
- `scan /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.
|
|
|
|
```sql
|
|
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)
|
|
|
|
```yaml
|
|
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
|
|
```
|