2026-08-29 01:43:12 +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.
|
|
|
|
|
|
2026-08-29 02:42:31 +00:00
|
|
|
**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
|
2026-08-29 03:01:19 +00:00
|
|
|
detection ("Match") is working, real online metadata lookups against TMDB ("Fetch
|
2026-08-29 03:27:19 +00:00
|
|
|
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
|
2026-08-29 03:36:06 +00:00
|
|
|
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.
|
2026-08-29 01:43:12 +00:00
|
|
|
|
2026-08-29 03:15:21 +00:00
|
|
|
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.
|
|
|
|
|
|
2026-08-29 01:43:12 +00:00
|
|
|
## 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](https://rhai.rs/) 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.
|
|
|
|
|
|
2026-08-29 03:15:21 +00:00
|
|
|
### 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.
|
|
|
|
|
|
2026-08-29 03:36:06 +00:00
|
|
|
`/` 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.
|
|
|
|
|
|
2026-08-29 01:43:12 +00:00
|
|
|
## Project layout
|
|
|
|
|
|
|
|
|
|
```
|
2026-08-29 02:42:31 +00:00
|
|
|
crates/renamr-core/ Pure Rust core: naming-expression engine (ported), local
|
2026-08-29 03:27:19 +00:00
|
|
|
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
|
2026-08-29 03:36:06 +00:00
|
|
|
`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).
|
2026-08-29 03:27:19 +00:00
|
|
|
No Tauri dependency, so it builds and unit-tests standalone.
|
2026-08-29 02:42:31 +00:00
|
|
|
src-tauri/ Tauri app shell: exposes renamr-core as commands to the frontend
|
2026-08-29 03:01:19 +00:00
|
|
|
(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).
|
2026-08-29 02:42:31 +00:00
|
|
|
src/ React + TypeScript UI (Vite). Faithful recreation of the original
|
|
|
|
|
two-pane rename screen (Aurora theme) wired to real file loading,
|
2026-08-29 03:01:19 +00:00
|
|
|
drag-and-drop, live naming-scheme preview, local matching, TMDB
|
|
|
|
|
fetch (behind a Settings modal for the API token), and a
|
2026-08-29 02:42:31 +00:00
|
|
|
confirm-before-touching-disk modal for the actual rename.
|
2026-08-29 01:43:12 +00:00
|
|
|
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).
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
# 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
|
2026-08-29 03:01:19 +00:00
|
|
|
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](https://www.themoviedb.org/) 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`.
|
2026-08-29 01:43:12 +00:00
|
|
|
|
|
|
|
|
## 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.
|
2026-08-29 02:42:31 +00:00
|
|
|
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
|
2026-08-29 03:27:19 +00:00
|
|
|
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.
|
2026-08-29 01:43:12 +00:00
|
|
|
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,
|
2026-08-29 03:01:19 +00:00
|
|
|
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
|
2026-08-29 03:27:19 +00:00
|
|
|
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.
|
2026-08-29 01:43:12 +00:00
|
|
|
6. **Rename engine + CLI** — `net.filebot.cli.CmdlineOperations` (1235 lines) and
|
|
|
|
|
`ArgumentBean` (415 lines, the full flag surface: `-rename`, `--db`, `--format`,
|
2026-08-29 03:36:06 +00:00
|
|
|
`--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.
|
2026-08-29 02:42:31 +00:00
|
|
|
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
|
2026-08-29 01:43:12 +00:00
|
|
|
`npm run tauri build`.
|