Skip to content

Latest commit

History

88 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

SQLite Renamer for Stash

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.

❗ Important ❗

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_LOG optionally writes a readable rename_log.txt audit trail; it is not an undo mechanism.

Requirements

  • Python 3.12–3.14 (the versions covered by CI)
  • A Stash database (.sqlite file)

Setup

  1. 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_LOG for a readable audit trail.

  2. Copy config.local.example.py to config.local.py (ignored by Git), then set DB_PATH, tags_dict, FALLBACK_TEMPLATE, and PATH_FILTER.

  3. Alternatively, keep private configuration elsewhere and pass --config /path/to/config.py, or set SQLITE_RENAMER_CONFIG.

  4. 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.

First Run (Dry Run)

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.py

This 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.json

It 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>.json

Undo 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>.json

Resume 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.

Filename Templates

Available variables: $date$performer$title$studio$height

TemplateResult
$titleHer Fantasy Ball.mp4
$title $heightHer Fantasy Ball 1080p.mp4
$date $title2016-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, COM1COM9, and LPT1LPT9) 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 4k and 8k; others as <height>p (e.g. 1080p).
  • If a scene has more than 3 performers, $performer is omitted. This applies before the optional FEMALE_ONLY filter.

Configuration

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.

Tag-to-template mapping

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

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"

Filter by path

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 everything

Run artifacts

All run artifacts are created next to the command's working directory and are ignored by Git.

FileWhen writtenContents
renamer_plan.jsonEvery planning runVersioned plan, timestamp, operations, and SHA-256 digest to review before applying
renamer_dryrun.txtEvery planning run; cleared at the start of each dry runConfiguration/tag summary, dedicated conflict details, and proposed old_path -> new_path renames with READY, NOOP, or BLOCKED status
renamer_runs/<uuid>.jsonNon-dry planning, apply, undo, and resumed applyAtomically 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.txtSuccessful apply when USING_LOG = TrueReadable 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.

Development and verification

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=80

GitHub 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.yml

requirements-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.

Performance baseline

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.

License

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.

About

Rename Stash video files from read-only SQLite metadata with configurable filename templates.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - NightScripted/sqlite-renamer: Rename Stash video files from read-only SQLite metadata with configurable filename templates. · GitHub
Skip to content

Latest commit

History

88 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

SQLite Renamer for Stash

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.

❗ Important ❗

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_LOG optionally writes a readable rename_log.txt audit trail; it is not an undo mechanism.

Requirements

  • Python 3.12–3.14 (the versions covered by CI)
  • A Stash database (.sqlite file)

Setup

  1. 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_LOG for a readable audit trail.

  2. Copy config.local.example.py to config.local.py (ignored by Git), then set DB_PATH, tags_dict, FALLBACK_TEMPLATE, and PATH_FILTER.

  3. Alternatively, keep private configuration elsewhere and pass --config /path/to/config.py, or set SQLITE_RENAMER_CONFIG.

  4. 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.

First Run (Dry Run)

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.py

This 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.json

It 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>.json

Undo 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>.json

Resume 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.

Filename Templates

Available variables: $date$performer$title$studio$height

TemplateResult
$titleHer Fantasy Ball.mp4
$title $heightHer Fantasy Ball 1080p.mp4
$date $title2016-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, COM1COM9, and LPT1LPT9) 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 4k and 8k; others as <height>p (e.g. 1080p).
  • If a scene has more than 3 performers, $performer is omitted. This applies before the optional FEMALE_ONLY filter.

Configuration

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.

Tag-to-template mapping

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

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"

Filter by path

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 everything

Run artifacts

All run artifacts are created next to the command's working directory and are ignored by Git.

FileWhen writtenContents
renamer_plan.jsonEvery planning runVersioned plan, timestamp, operations, and SHA-256 digest to review before applying
renamer_dryrun.txtEvery planning run; cleared at the start of each dry runConfiguration/tag summary, dedicated conflict details, and proposed old_path -> new_path renames with READY, NOOP, or BLOCKED status
renamer_runs/<uuid>.jsonNon-dry planning, apply, undo, and resumed applyAtomically 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.txtSuccessful apply when USING_LOG = TrueReadable 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.

Development and verification

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=80

GitHub 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.yml

requirements-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.

Performance baseline

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.

License

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.

About

Rename Stash video files from read-only SQLite metadata with configurable filename templates.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - NightScripted/sqlite-renamer: Rename Stash video files from read-only SQLite metadata with configurable filename templates. · GitHub
Skip to content

Latest commit

History

88 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

SQLite Renamer for Stash

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.

❗ Important ❗

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_LOG optionally writes a readable rename_log.txt audit trail; it is not an undo mechanism.

Requirements

  • Python 3.12–3.14 (the versions covered by CI)
  • A Stash database (.sqlite file)

Setup

  1. 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_LOG for a readable audit trail.

  2. Copy config.local.example.py to config.local.py (ignored by Git), then set DB_PATH, tags_dict, FALLBACK_TEMPLATE, and PATH_FILTER.

  3. Alternatively, keep private configuration elsewhere and pass --config /path/to/config.py, or set SQLITE_RENAMER_CONFIG.

  4. 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.

First Run (Dry Run)

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.py

This 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.json

It 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>.json

Undo 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>.json

Resume 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.

Filename Templates

Available variables: $date$performer$title$studio$height

TemplateResult
$titleHer Fantasy Ball.mp4
$title $heightHer Fantasy Ball 1080p.mp4
$date $title2016-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, COM1COM9, and LPT1LPT9) 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 4k and 8k; others as <height>p (e.g. 1080p).
  • If a scene has more than 3 performers, $performer is omitted. This applies before the optional FEMALE_ONLY filter.

Configuration

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.

Tag-to-template mapping

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

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"

Filter by path

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 everything

Run artifacts

All run artifacts are created next to the command's working directory and are ignored by Git.

FileWhen writtenContents
renamer_plan.jsonEvery planning runVersioned plan, timestamp, operations, and SHA-256 digest to review before applying
renamer_dryrun.txtEvery planning run; cleared at the start of each dry runConfiguration/tag summary, dedicated conflict details, and proposed old_path -> new_path renames with READY, NOOP, or BLOCKED status
renamer_runs/<uuid>.jsonNon-dry planning, apply, undo, and resumed applyAtomically 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.txtSuccessful apply when USING_LOG = TrueReadable 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.

Development and verification

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=80

GitHub 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.yml

requirements-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.

Performance baseline

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.

License

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.

About

Rename Stash video files from read-only SQLite metadata with configurable filename templates.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - NightScripted/sqlite-renamer: Rename Stash video files from read-only SQLite metadata with configurable filename templates. · GitHub
Skip to content

Latest commit

History

88 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

SQLite Renamer for Stash

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.

❗ Important ❗

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_LOG optionally writes a readable rename_log.txt audit trail; it is not an undo mechanism.

Requirements

  • Python 3.12–3.14 (the versions covered by CI)
  • A Stash database (.sqlite file)

Setup

  1. 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_LOG for a readable audit trail.

  2. Copy config.local.example.py to config.local.py (ignored by Git), then set DB_PATH, tags_dict, FALLBACK_TEMPLATE, and PATH_FILTER.

  3. Alternatively, keep private configuration elsewhere and pass --config /path/to/config.py, or set SQLITE_RENAMER_CONFIG.

  4. 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.

First Run (Dry Run)

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.py

This 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.json

It 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>.json

Undo 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>.json

Resume 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.

Filename Templates

Available variables: $date$performer$title$studio$height

TemplateResult
$titleHer Fantasy Ball.mp4
$title $heightHer Fantasy Ball 1080p.mp4
$date $title2016-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, COM1COM9, and LPT1LPT9) 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 4k and 8k; others as <height>p (e.g. 1080p).
  • If a scene has more than 3 performers, $performer is omitted. This applies before the optional FEMALE_ONLY filter.

Configuration

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.

Tag-to-template mapping

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

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"

Filter by path

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 everything

Run artifacts

All run artifacts are created next to the command's working directory and are ignored by Git.

FileWhen writtenContents
renamer_plan.jsonEvery planning runVersioned plan, timestamp, operations, and SHA-256 digest to review before applying
renamer_dryrun.txtEvery planning run; cleared at the start of each dry runConfiguration/tag summary, dedicated conflict details, and proposed old_path -> new_path renames with READY, NOOP, or BLOCKED status
renamer_runs/<uuid>.jsonNon-dry planning, apply, undo, and resumed applyAtomically 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.txtSuccessful apply when USING_LOG = TrueReadable 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.

Development and verification

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=80

GitHub 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.yml

requirements-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.

Performance baseline

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.

License

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.

About

Rename Stash video files from read-only SQLite metadata with configurable filename templates.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - NightScripted/sqlite-renamer: Rename Stash video files from read-only SQLite metadata with configurable filename templates. · GitHub
Skip to content

Latest commit

History

88 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

SQLite Renamer for Stash

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.

❗ Important ❗

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_LOG optionally writes a readable rename_log.txt audit trail; it is not an undo mechanism.

Requirements

  • Python 3.12–3.14 (the versions covered by CI)
  • A Stash database (.sqlite file)

Setup

  1. 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_LOG for a readable audit trail.

  2. Copy config.local.example.py to config.local.py (ignored by Git), then set DB_PATH, tags_dict, FALLBACK_TEMPLATE, and PATH_FILTER.

  3. Alternatively, keep private configuration elsewhere and pass --config /path/to/config.py, or set SQLITE_RENAMER_CONFIG.

  4. 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.

First Run (Dry Run)

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.py

This 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.json

It 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>.json

Undo 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>.json

Resume 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.

Filename Templates

Available variables: $date$performer$title$studio$height

TemplateResult
$titleHer Fantasy Ball.mp4
$title $heightHer Fantasy Ball 1080p.mp4
$date $title2016-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, COM1COM9, and LPT1LPT9) 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 4k and 8k; others as <height>p (e.g. 1080p).
  • If a scene has more than 3 performers, $performer is omitted. This applies before the optional FEMALE_ONLY filter.

Configuration

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.

Tag-to-template mapping

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

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"

Filter by path

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 everything

Run artifacts

All run artifacts are created next to the command's working directory and are ignored by Git.

FileWhen writtenContents
renamer_plan.jsonEvery planning runVersioned plan, timestamp, operations, and SHA-256 digest to review before applying
renamer_dryrun.txtEvery planning run; cleared at the start of each dry runConfiguration/tag summary, dedicated conflict details, and proposed old_path -> new_path renames with READY, NOOP, or BLOCKED status
renamer_runs/<uuid>.jsonNon-dry planning, apply, undo, and resumed applyAtomically 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.txtSuccessful apply when USING_LOG = TrueReadable 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.

Development and verification

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=80

GitHub 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.yml

requirements-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.

Performance baseline

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.

License

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.

About

Rename Stash video files from read-only SQLite metadata with configurable filename templates.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - NightScripted/sqlite-renamer: Rename Stash video files from read-only SQLite metadata with configurable filename templates. · GitHub
Skip to content

Latest commit

History

88 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

SQLite Renamer for Stash

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.

❗ Important ❗

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_LOG optionally writes a readable rename_log.txt audit trail; it is not an undo mechanism.

Requirements

  • Python 3.12–3.14 (the versions covered by CI)
  • A Stash database (.sqlite file)

Setup

  1. 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_LOG for a readable audit trail.

  2. Copy config.local.example.py to config.local.py (ignored by Git), then set DB_PATH, tags_dict, FALLBACK_TEMPLATE, and PATH_FILTER.

  3. Alternatively, keep private configuration elsewhere and pass --config /path/to/config.py, or set SQLITE_RENAMER_CONFIG.

  4. 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.

First Run (Dry Run)

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.py

This 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.json

It 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>.json

Undo 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>.json

Resume 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.

Filename Templates

Available variables: $date$performer$title$studio$height

TemplateResult
$titleHer Fantasy Ball.mp4
$title $heightHer Fantasy Ball 1080p.mp4
$date $title2016-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, COM1COM9, and LPT1LPT9) 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 4k and 8k; others as <height>p (e.g. 1080p).
  • If a scene has more than 3 performers, $performer is omitted. This applies before the optional FEMALE_ONLY filter.

Configuration

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.

Tag-to-template mapping

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

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"

Filter by path

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 everything

Run artifacts

All run artifacts are created next to the command's working directory and are ignored by Git.

FileWhen writtenContents
renamer_plan.jsonEvery planning runVersioned plan, timestamp, operations, and SHA-256 digest to review before applying
renamer_dryrun.txtEvery planning run; cleared at the start of each dry runConfiguration/tag summary, dedicated conflict details, and proposed old_path -> new_path renames with READY, NOOP, or BLOCKED status
renamer_runs/<uuid>.jsonNon-dry planning, apply, undo, and resumed applyAtomically 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.txtSuccessful apply when USING_LOG = TrueReadable 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.

Development and verification

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=80

GitHub 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.yml

requirements-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.

Performance baseline

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.

License

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.

About

Rename Stash video files from read-only SQLite metadata with configurable filename templates.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - NightScripted/sqlite-renamer: Rename Stash video files from read-only SQLite metadata with configurable filename templates. · GitHub
Skip to content

Latest commit

History

88 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

SQLite Renamer for Stash

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.

❗ Important ❗

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_LOG optionally writes a readable rename_log.txt audit trail; it is not an undo mechanism.

Requirements

  • Python 3.12–3.14 (the versions covered by CI)
  • A Stash database (.sqlite file)

Setup

  1. 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_LOG for a readable audit trail.

  2. Copy config.local.example.py to config.local.py (ignored by Git), then set DB_PATH, tags_dict, FALLBACK_TEMPLATE, and PATH_FILTER.

  3. Alternatively, keep private configuration elsewhere and pass --config /path/to/config.py, or set SQLITE_RENAMER_CONFIG.

  4. 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.

First Run (Dry Run)

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.py

This 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.json

It 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>.json

Undo 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>.json

Resume 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.

Filename Templates

Available variables: $date$performer$title$studio$height

TemplateResult
$titleHer Fantasy Ball.mp4
$title $heightHer Fantasy Ball 1080p.mp4
$date $title2016-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, COM1COM9, and LPT1LPT9) 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 4k and 8k; others as <height>p (e.g. 1080p).
  • If a scene has more than 3 performers, $performer is omitted. This applies before the optional FEMALE_ONLY filter.

Configuration

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.

Tag-to-template mapping

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

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"

Filter by path

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 everything

Run artifacts

All run artifacts are created next to the command's working directory and are ignored by Git.

FileWhen writtenContents
renamer_plan.jsonEvery planning runVersioned plan, timestamp, operations, and SHA-256 digest to review before applying
renamer_dryrun.txtEvery planning run; cleared at the start of each dry runConfiguration/tag summary, dedicated conflict details, and proposed old_path -> new_path renames with READY, NOOP, or BLOCKED status
renamer_runs/<uuid>.jsonNon-dry planning, apply, undo, and resumed applyAtomically 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.txtSuccessful apply when USING_LOG = TrueReadable 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.

Development and verification

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=80

GitHub 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.yml

requirements-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.

Performance baseline

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.

License

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.

About

Rename Stash video files from read-only SQLite metadata with configurable filename templates.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - NightScripted/sqlite-renamer: Rename Stash video files from read-only SQLite metadata with configurable filename templates. · GitHub
Skip to content

Latest commit

History

88 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

SQLite Renamer for Stash

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.

❗ Important ❗

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_LOG optionally writes a readable rename_log.txt audit trail; it is not an undo mechanism.

Requirements

  • Python 3.12–3.14 (the versions covered by CI)
  • A Stash database (.sqlite file)

Setup

  1. 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_LOG for a readable audit trail.

  2. Copy config.local.example.py to config.local.py (ignored by Git), then set DB_PATH, tags_dict, FALLBACK_TEMPLATE, and PATH_FILTER.

  3. Alternatively, keep private configuration elsewhere and pass --config /path/to/config.py, or set SQLITE_RENAMER_CONFIG.

  4. 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.

First Run (Dry Run)

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.py

This 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.json

It 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>.json

Undo 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>.json

Resume 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.

Filename Templates

Available variables: $date$performer$title$studio$height

TemplateResult
$titleHer Fantasy Ball.mp4
$title $heightHer Fantasy Ball 1080p.mp4
$date $title2016-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, COM1COM9, and LPT1LPT9) 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 4k and 8k; others as <height>p (e.g. 1080p).
  • If a scene has more than 3 performers, $performer is omitted. This applies before the optional FEMALE_ONLY filter.

Configuration

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.

Tag-to-template mapping

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

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"

Filter by path

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 everything

Run artifacts

All run artifacts are created next to the command's working directory and are ignored by Git.

FileWhen writtenContents
renamer_plan.jsonEvery planning runVersioned plan, timestamp, operations, and SHA-256 digest to review before applying
renamer_dryrun.txtEvery planning run; cleared at the start of each dry runConfiguration/tag summary, dedicated conflict details, and proposed old_path -> new_path renames with READY, NOOP, or BLOCKED status
renamer_runs/<uuid>.jsonNon-dry planning, apply, undo, and resumed applyAtomically 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.txtSuccessful apply when USING_LOG = TrueReadable 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.

Development and verification

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=80

GitHub 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.yml

requirements-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.

Performance baseline

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.

License

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.

About

Rename Stash video files from read-only SQLite metadata with configurable filename templates.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages