Renamr Scaffold 658a235e05 Let the naming scheme organize files into subfolders
- rename::rename_one now resolves new_name relative to the source file's
  own directory, allowing / as a subdirectory separator (e.g. a naming
  scheme like {show name}/{episode name}) instead of rejecting it. Missing
  intermediate directories are created automatically via create_dir_all.
- New validate_relative_target() replaces the old is_plain_filename():
  still rejects anything that could escape the source directory (empty
  path segments - which also catches a leading-slash absolute path -
  and . / .. segments) plus backslashes (Renamr always treats / as the
  scheme's folder separator regardless of OS), but now allows a
  legitimate multi-segment relative path through.
- The "Unchanged" check now compares the full resolved target path to the
  source, not just the filename, since a computed name can now include
  directory components.
- 6 new tests: organizing into a new subfolder, nested subfolders that
  don't exist yet, conflict detection still works for organized targets,
  and rejection of backslashes / doubled / leading / trailing slashes.
- Frontend: naming-scheme editor hint explains the / convention, and the
  rename confirmation modal notes when a plan will create new subfolders.
- No changes needed in the naming-expression engine itself - it already
  treats / between two {} scriptlets as ordinary literal text.
2026-08-29 03:36:06 +00:00

Renamr

A fast, cross-platform media renamer — a ground-up rewrite of FileBot (via its FB-Mod fork), built from scratch as its own project: new stack, new architecture, new branding.

Status: early scaffold, but the core loop works end to end. The naming-expression engine is ported and tested, the faithfully-recreated two-pane UI is wired to real native file/folder loading and drag-and-drop, local filename-based season/episode detection ("Match") is working, real online metadata lookups against TMDB ("Fetch Data") are working — including fuzzy show-name disambiguation, so a search that returns several similarly-named shows doesn't just take TMDB's top-ranked hit — and the actual on-disk rename operation is wired up (skip-on-conflict, with a confirmation step showing the full old-name/new-name plan before anything touches disk — there is no undo yet). Rename can also organize files into subfolders — including ones that don't exist yet — when the naming scheme includes a /. Full media-metadata bindings are not yet ported. See "Porting roadmap" below.

The naming scheme is fully user-editable from the app (the "Naming Scheme" button next to the New Names list) — the default is {show name} - {season number}x{episode number} - {episode name}, but it's just a starting point, not a fixed template. Plain, human-readable, multi-word field names like {show name} work directly (no Rhai knowledge needed), FileBot's own short field names ({n}, {s00e00}, {t}) still work side by side with them, and real Rhai script is available for anyone who wants it (e.g. {show name.to_upper()}). See "Naming scheme syntax" below for how that's implemented.

Why a rewrite, and why this stack

The original is a ~20k+ line Java/Swing app built with Ant. The goals were: faster startup and UI, faster matching/renaming, a codebase that's actually pleasant to work in, and a genuine rebrand — not a coat of paint.

Renamr is Tauri: a Rust core (crates/renamr-core, src-tauri) plus a TypeScript/React frontend (src), compiled to a small native binary per platform (Windows/macOS/Linux) with a system webview instead of shipping a bundled browser engine. That's the concrete "faster" win over both the original JVM/Swing app and an Electron-style rewrite: quick cold start, small binary, real OS-native windowing.

An earlier attempt used Kotlin + Compose Multiplatform to stay on the JVM and reuse FileBot's Java logic directly. That was abandoned (nothing was salvaged into this repo) after discovering the sandbox this was originally built in blocks Maven Central and the Gradle Plugin Portal, making it impossible to compile or test. Tauri was chosen as the replacement because both halves — Cargo via crates.io, npm via the public npm registry — are reachable, so every piece described as "done" below is actually compiled/tested, not just written and hoped for.

Compatibility notes

Goal: keep CLI flags, config file locations, and the outer { } naming-scheme template syntax compatible with FileBot conventions, so existing automation mostly keeps working.

One deliberate gap: FileBot's { } expressions ran arbitrary Groovy code (JVM-only). Renamr's Rust core has no Groovy engine, so expressions are evaluated with Rhai instead. Simple bindings ({n}, {s00e00}, {t}) are identical. More complex chained-method expressions will need small syntax tweaks (e.g. Groovy's .toUpperCase() → Rhai's .to_upper()) — Rhai already ships most of the common string/array methods FileBot's expression helpers wrapped, so most patterns translate directly.

Naming scheme syntax

Every { } token in an expression is resolved in this order (crates/renamr-core/src/format/expression.rs::compile_scriptlet):

  1. A single bare identifier ({n}, {season}) is a direct binding lookup — the fast path, and how FileBot's own short field names keep working unmodified.
  2. Otherwise it's compiled as Rhai script ({n.to_upper()}, {n + " " + t}) — real code, with every current binding available as a script variable.
  3. If it's neither a valid identifier nor valid Rhai, but it still only contains letters, digits, underscores and spaces (e.g. {show name}, {episode number}), it falls back to a literal multi-word binding key instead of raising a syntax error — this is what lets a human-typed field name with spaces work as a plain lookup without anyone needing to know Rhai. Anything that isn't a plausible field name (stray punctuation, unmatched brackets, etc.) still surfaces as a real syntax error at that point, exactly as before.

An undefined binding contributes an empty string rather than failing outright (so {n}{missing} still renders); the overall expression only errors if the total rendered output would be empty.

/ between two { } scriptlets (or anywhere in the literal text) needs no special handling in the expression engine at all — it's just ordinary literal text, the same as a space or a dash. It only becomes meaningful one layer up, at rename time (see "Organizing into subfolders" below): a scheme like {show name}/{episode name} renders to a name containing a real /, and the rename engine treats that as "put this file in a subfolder" rather than rejecting it.

Organizing into subfolders

A naming scheme that includes / (e.g. {show name}/Season {season number}/{episode number} - {episode name}) moves the file into that subfolder as part of renaming, creating any missing intermediate folders automatically (crates/renamr-core/src/rename/mod.rs::rename_one → fs::create_dir_all). This is still always scoped to the file's own directory tree — the target is resolved relative to the source file's parent directory, never as an absolute path, and every path component is validated (validate_relative_target) to reject empty segments (a leading, trailing, or doubled / — which also catches an absolute-looking /etc/passwd-style path), ./.. segments, and backslashes (Renamr always uses / for the naming scheme's folder separator regardless of OS). Conflict policy is unchanged: if the resolved target already exists and isn't the same underlying file, that item is skipped rather than overwritten.

Project layout

crates/renamr-core/   Pure Rust core: naming-expression engine (ported), local
                       filename-based season/episode matching (ported, `matching/`),
                       text-similarity scoring for disambiguating search results
                       (`similarity/`, partial `EpisodeMetrics` port), a TMDB metadata
                       client (`web/tmdb.rs` — TV search + episode lookup, disambiguated
                       via `similarity/`, tested against a mocked HTTP server via
                       `mockito`), and the on-disk rename engine (`rename/`) —
                       skip-on-conflict, can also organize into (auto-created)
                       subfolders when the naming scheme includes `/`. Not yet ported:
                       full media metadata bindings, other web datasource clients
                       (TVDB/AniDB/etc).
                       No Tauri dependency, so it builds and unit-tests standalone.
src-tauri/             Tauri app shell: exposes renamr-core as commands to the frontend
                       (naming-scheme rendering, filename matching, TMDB metadata fetch,
                       executing a rename plan, native file/folder dialogs, drag-and-drop
                       expansion), plus `settings.rs` (TMDB token, stored as JSON in the
                       OS app-config directory), app metadata/icons/window config
                       (tauri.conf.json).
src/                   React + TypeScript UI (Vite). Faithful recreation of the original
                       two-pane rename screen (Aurora theme) wired to real file loading,
                       drag-and-drop, live naming-scheme preview, local matching, TMDB
                       fetch (behind a Settings modal for the API token), and a
                       confirm-before-touching-disk modal for the actual rename.
scripts/gen_icons.py   Regenerates src-tauri/icons/ (placeholder branding — swap the
                       colors/glyph in the script, or replace the icons directly, to
                       change the look).

Building

Prerequisites: Rust (stable) + Cargo, Node.js 18+, and Tauri's platform prerequisites (see https://v2.tauri.app/start/prerequisites/ — on Linux this means libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev and a C toolchain; macOS/Windows need Xcode / the VS Build Tools + WebView2 respectively).

# Rust core (no UI deps needed):
cargo test -p renamr-core

# Full app in dev mode (opens a native window):
npm install
npm run tauri dev

# Production build (installer/bundle for your current OS):
npm run tauri build

Verified in the environment this was built in: cargo test -p renamr-core (all passing, including the TMDB client tests — those run against a local mock HTTP server via mockito, no live network or API key needed to build/test), cargo check -p renamr (compiles clean against real Tauri + GTK/WebKit system libraries), and npm run build (TypeScript type-checks, Vite bundles). The actual GUI window has not been visually verified anywhere yet — that sandbox has no display. Run npm run tauri dev on your machine to see and click through it for the first time.

Setting up TMDB (for "Fetch Data")

Everything else in the app works fully offline; "Fetch Data" is the one feature that needs a (free) TMDB account:

  1. Create a TMDB account, then go to Settings → API in your account and generate an API Read Access Token (not the older numeric "API Key" — Renamr uses the Bearer token method TMDB documents as the default).
  2. In the app, click the key icon ("TMDB Settings") next to Fetch Data and paste the token in. It's saved locally in the OS app-config directory and only ever sent to api.themoviedb.org.

Porting roadmap (not yet done)

Roughly in priority order — each references the original FileBot source file it's ported from, for anyone picking this up:

  1. Full naming-scheme bindings — net.filebot.format.MediaBindingBean (1354 lines): all the {n}, {s00e00}, {airdate}, {vf} (video format), etc. binding properties. Currently only whatever bindings you pass in manually are available.
  2. String/expression helper methods — net.filebot.format.ExpressionFormatMethods (636 lines): FileBot-specific Groovy string extensions like .pad(), .sortName(). Only a small subset (any, all_of, concat) is ported so far (crates/renamr-core/src/format/functions.rs); Rhai's built-ins cover some of the rest already.
  3. Matching/similarity engine — net.filebot.similarity.*. Local filename-based season/episode detection is done (crates/renamr-core/src/matching, ported from the pattern-matching half of SeasonEpisodeMatcher.java, adapted to Rust's regex crate which lacks lookahead/lookbehind — wired to the "Match" button). Show-name disambiguation is also done (crates/renamr-core/src/similarity, a partial port of the scoring idea behind EpisodeMetrics.java's fuzzy title-matching cascade — Sorensen-Dice similarity over normalized-title character bigrams): "Fetch Data" now picks whichever TMDB search result is textually closest to the local guess rather than just trusting TMDB's relevance/popularity-ranked first hit, and reports "not found" instead of guessing when nothing in the results is a plausible match at all. DateMatcher (numbering/air-date-based identification, the other half of the original EpisodeMetrics) is not ported — Renamr doesn't do date-based episode identification.
  4. Media detection — net.filebot.media.MediaDetection (1356 lines): ties file-name parsing + matching together to auto-identify what a file is.
  5. Web datasource clients — net.filebot.web.* (~40 files): TheMovieDB, TheTVDB, AniDB, OMDb, TVmaze, OpenSubtitles, etc. TMDB is done for TV (crates/renamr-core/src/web/tmdb.rs, reqwest + serde_json, Bearer-token auth) — search-by-name plus per-season episode lookup, wired to "Fetch Data", disambiguating multi-result searches via the similarity-scoring pass described in item 3 above rather than blindly trusting TMDB's top-ranked hit. TMDB movie lookup and all other sources (TheTVDB, AniDB, OMDb, TVmaze, OpenSubtitles) are not yet started.
  6. Rename engine + CLI — net.filebot.cli.CmdlineOperations (1235 lines) and ArgumentBean (415 lines, the full flag surface: -rename, --db, --format, --action, --conflict, -get-subtitles, -script, etc.). The rename operation is done (crates/renamr-core/src/rename, fixed skip-on-conflict policy, wired to the UI's Rename button behind a confirmation step) including the move-into-subfolder --action variant — a naming scheme like {n}/{s00e00} organizes files into subfolders, creating any missing folders automatically, always scoped to the file's own directory tree. Not yet done: the copy/symlink --action variants, the --conflict variants beyond skip (auto-number, override), and the CLI flag surface itself.
  7. The rest of the UI — the Rename panel (file loading, drag-drop, live preview, local matching, confirmed rename) is built; the other five sidebar panels (Episodes/Subtitles/SFV/Filter/List) are still stub screens, plus Align up/down reordering, History, and Presets on the Rename panel itself are no-ops. There's also no per-item exclude inside the rename confirmation modal yet (it's all-or-nothing — use "Exclude Selected" on the file list beforehand to leave a file out entirely).
  8. Undo/history for renames — there is no undo yet; the confirmation modal before Rename executes is the only safety net right now. net.filebot.HistoryDialog/the original's rename-history log is the reference for what a real undo would look like.
  9. Packaging — code signing, auto-update, installers per platform via npm run tauri build.
S
Description
No description provided
Readme
4.5 MiB
2026-08-30 12:26:15 +00:00
Languages
Rust 61.8%
TypeScript 31.6%
CSS 6.3%
Python 0.3%