Files
StashHandler/summary.md
T
bvandeusen f09d94e34b Initial commit: StashHandler
Bridge between qBittorrent and Stash that polls for completed torrents,
deduplicates via SHA256 hashing and SQLite, and imports new media files.
Supports daemon mode (continuous polling) and scan mode (bulk operations).
Configuration via environment variables for Docker Swarm compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 11:25:05 -05:00

4.1 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, hashes media files, deduplicates them via a SQLite database, and moves new files into Stash's import directory.

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 Example compose with stash-handler, qbittorrent, stash services
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
  • 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 move File transfer mode: move, hardlink, or copy
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