Historical community discussion: https://discourse.stashapp.cc/t/sqlite-renamer-for-stash/1476. It describes an earlier version; this repository's documentation is authoritative.
Uses metadata from your Stash SQLite database to rename your video files on disk.
This will make permanent changes to your files on disk. The SQLite database is read-only — the script never writes to it.
A completed v2 or v3 run manifest is the safe undo record.
USING_LOGoptionally writes a readablerename_log.txtaudit trail; it is not an undo mechanism.
- Python 3.12–3.14 (the versions covered by CI)
- A Stash database (
.sqlitefile)
Back up your video files before a live run. Keep the v3 run manifest private and retain it if you may need safe undo or recovery; optionally enable
USING_LOGfor a readable audit trail.Copy
config.local.example.pytoconfig.local.py(ignored by Git), then setDB_PATH,tags_dict,FALLBACK_TEMPLATE, andPATH_FILTER.Alternatively, keep private configuration elsewhere and pass
--config /path/to/config.py, or setSQLITE_RENAMER_CONFIG.Install the project requirements:
python -m pip install -r requirements.txt
For an isolated source installation, use pipx install .; it provides the sqlite-renamer command. No package has been published yet.
DRY_RUN defaults to True in config.py, so a first run does not rename media files. Keep it enabled until you have reviewed the output.
Run:
python run_renamer.pyThis writes renamer_plan.json and renamer_dryrun.txt. The terminal preview and dry-run file start with a configuration/tag summary, then show ready, no-op, and blocked operations after checking source files, occupied destinations, directory containment, and collisions across the complete batch. Blockers are collected in a dedicated conflict section with their reason; nothing is renamed from this interface. A configured tag that is absent from Stash is listed as MISSING TAG; a present tag with no scenes is listed as EMPTY TAG; and a tag whose scenes were already claimed by an earlier configured rule is listed as SHADOWED TAG. It does not create a run manifest. Set STOP_AFTER_FIRST = True to limit each matching tag or fallback pass to one scene.
To recheck a saved plan later without applying it, use:
python run_renamer.py --preview-plan renamer_plan.jsonIt revalidates the current filesystem state and displays the same plan/conflict preview.
For a live run, back up the files, review the dry-run output, then explicitly set DRY_RUN = False and run the same command again. To apply a reviewed plan explicitly, run python run_renamer.py --apply-plan renamer_plan.json; its digest and filesystem state are revalidated before any rename. A live run never writes to the SQLite database.
To undo one completed v2 or v3 apply run, keep DRY_RUN = False and pass its run manifest:
sqlite-renamer --undo-manifest renamer_runs/<uuid>.jsonUndo re-hashes each applied destination and refuses to replace an occupied original path. It writes a new undone manifest linked to the original run. Version 1 manifests lack the required fingerprints and cannot be undone automatically.
If an apply is interrupted or fails after safely rolling back earlier operations, leave its incomplete v3 manifest in place. After reviewing the filesystem, keep DRY_RUN = False and use:
python run_renamer.py --resume-manifest renamer_runs/<uuid>.jsonResume verifies every recorded completed destination and every pending or rollback-related source against its saved SHA-256 before applying only the remaining work. If a failed rollback leaves verified duplicate source and destination paths, resume removes the duplicate source and records the operation as applied; changed, missing, or conflicting paths still block. Resume never regenerates the plan or rereads tag rules.
Available variables: $date$performer$title$studio$height
| Template | Result |
|---|---|
$title | Her Fantasy Ball.mp4 |
$title $height | Her Fantasy Ball 1080p.mp4 |
$date $title | 2016-12-29 Her Fantasy Ball.mp4 |
$date $performer - $title [$studio] | 2016-12-29 Eva Lovia - Her Fantasy Ball [Sneaky Sex].mp4 |
Notes:
- Illegal Windows filename characters, leading/trailing ASCII spaces, and trailing periods are stripped automatically. Reserved device names (including
CON,NUL,COM1–COM9, andLPT1–LPT9) block the plan.#and,are also stripped even though they are legal on Windows — edit the character-stripping regex in the script to preserve them. - Heights of 2160 and 4320 are shown as
4kand8k; others as<height>p(e.g.1080p). - If a scene has more than 3 performers,
$performeris omitted. This applies before the optionalFEMALE_ONLYfilter.
config.py contains safe distributable defaults. Put personal settings in the ignored config.local.py, pass --config PATH, or set SQLITE_RENAMER_CONFIG; explicit --config has highest precedence.
tags_dict maps each Stash tag to a filename template. Tag passes run in dictionary order, and the first matching configured tag claims each scene. Later tag passes skip already claimed scenes; the fallback template applies only to scenes that no configured tag claimed.
Each rule must be a dictionary with non-empty string tag and filename values. Invalid rules stop planning before the database is opened.
Tag names below are examples — replace them with your actual Stash tag names.
tags_dict= {
"1": {"tag": "!1. JAV", "filename": "$title"},
"2": {"tag": "!1. Anime", "filename": "$date $title"},
"3": {"tag": "!1. Western", "filename": "$date $performer - $title [$studio]"},
}FALLBACK_TEMPLATE is applied to every scene that does not match any tag in tags_dict. Set it to "" to skip untagged scenes entirely.
FALLBACK_TEMPLATE="$studio - $date - $performer - $title"PATH_FILTER limits all passes (tag and fallback) to files whose folder path matches a SQL LIKE pattern. Set to "" to process all scenes regardless of location.
PATH_FILTER=r"E:\Film\R18\%"# only files under E:\Film\R18\PATH_FILTER=""# no filter — process everythingAll run artifacts are created next to the command's working directory and are ignored by Git.
| File | When written | Contents |
|---|---|---|
renamer_plan.json | Every planning run | Versioned plan, timestamp, operations, and SHA-256 digest to review before applying |
renamer_dryrun.txt | Every planning run; cleared at the start of each dry run | Configuration/tag summary, dedicated conflict details, and proposed old_path -> new_path renames with READY, NOOP, or BLOCKED status |
renamer_runs/<uuid>.json | Non-dry planning, apply, undo, and resumed apply | Atomically written v3 manifest with action, timestamps, configuration/plan digests, completion state, exception record, per-operation result/error, source/completed-target SHA-256, and (for undo) the parent run ID |
rename_log.txt | Successful apply when USING_LOG = True | Readable scene_id|old_path|new_path audit trail; use the v2/v3 manifest for recovery |
renamer_runs/ is created only with DRY_RUN = False; dry runs create no manifests. Manifests include media paths, metadata-derived filenames, and file hashes, so treat them as private run records and keep them out of version control. The utility never deletes manifests: retain an apply manifest until safe undo/recovery is no longer needed, then archive or remove it manually. rename_log.txt remains an optional readable audit trail, not a recovery record.
Install both dependency sets and run the same coverage gate used by CI:
python -m pip install -r requirements.txt -r requirements-dev.txt
python -m pytest tests/ -v --cov=. --cov-report=term-missing --cov-fail-under=80GitHub Actions runs this check on Python 3.12, 3.13, and 3.14. Dependabot checks Python packages weekly and GitHub Actions monthly.
The repository also has a deliberately small, reproducible quality baseline:
python -m ruff check .
python -m ruff format --check --exclude README.md .
python -m mypy
python -m yamllint .github .yamllint.yml
python -m interrogate .
actionlint -color .github/workflows/ci.ymlrequirements-dev.txt pins Ruff, mypy, yamllint, and Interrogate. Interrogate enforces at least 80% docstring coverage. Install Actionlint v1.7.12 from its release page or with your package manager; CI installs that exact version with go install.
For contribution, security-reporting, release-preparation, and historical-status guidance, see CONTRIBUTING.md, SECURITY.md, RELEASING.md, ROADMAP.md, and ANALYSIS.md.
Run python benchmarks/benchmark_planning.py --sizes 100,1000 to measure planning time, SQL statement count, and peak Python allocation using invented SQLite data only. See benchmarks/README.md for the current baseline and interpretation.
This project is licensed under the GNU General Public License v3.0 or later. You may use, modify, and distribute it—including commercially—provided that distributed derivative works remain available under the same license and their corresponding source is made available under GPL terms.