Files
StashHandler/summary.md
T
bvandeusen 6adfd77236 Add category filtering, default to copy mode, split dev/prod compose
- 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>
2026-02-16 11:38:17 -05:00

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
```