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