Align Up/Down now swaps the selected file with its neighbor in the shared item list (Original Files and New Names stay positionally paired, matching the original FileBot's "align" buttons for manually lining up mismatched lists). Naming-scheme Presets: up to 9 saved expressions, one per digit slot, persisted as presets.json (src-tauri/src/presets.rs, same load/save pattern as settings.rs/history.rs). A new Presets panel lists all 9 slots with Save Current / Apply / Clear, and a global 1-9 keydown handler applies a slot instantly from anywhere in the app (ignored while a text field is focused or a modal is open).
273 lines
18 KiB
Markdown
273 lines
18 KiB
Markdown
# 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 *and
|
|
movie title/year* detection ("Match") is working, real online metadata lookups against
|
|
TMDB ("Fetch Data") are working for both TV episodes and movies — including fuzzy
|
|
title disambiguation, so a search that returns several similarly-named results doesn't
|
|
just take TMDB's top-ranked hit — and the actual on-disk operation is wired up
|
|
(skip-on-conflict, with a confirmation step showing the full old-path/new-path plan
|
|
before anything touches disk, a per-row ✕ to drop an individual file from that plan at
|
|
the last step, and a Move/Copy/Symlink selector for what to actually do at each
|
|
target). It can also organize files into subfolders — including ones that don't exist
|
|
yet — when the naming scheme includes a `/`. Every completed batch is logged to a
|
|
history panel that can undo it (moving files back for Move, or just removing what was
|
|
created for Copy/Symlink). A single loaded batch can freely mix TV episodes and movies
|
|
— each file only fills in the fields that apply to it. "Match" also parses release-tag
|
|
text straight out of the filename — resolution, source (BluRay/HDTV/WEB-DL/…),
|
|
video/audio codec, release group — so naming schemes can reference
|
|
`{resolution}`/`{source}`/`{video codec}`/`{audio codec}`/`{release group}` (or
|
|
FileBot's own `{vf}`/`{source}`/`{vc}`/`{ac}`/`{group}`) without a media prober; the
|
|
rest of FileBot's full media-metadata bindings (real probed video/audio stream info)
|
|
are not yet ported. The Align Up/Down buttons reorder the (shared) file list — the two
|
|
panes always stay positionally paired — and up to 9 naming schemes can be saved as
|
|
presets, recalled with a click or a `1`-`9` keypress (persisted locally, same pattern as
|
|
Settings/History). 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](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.
|
|
|
|
### 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 *and movie title/year* matching
|
|
(ported, `matching/`), plus filename-based release-tag parsing in
|
|
the same module (`guess_release_info` — resolution, source, video/
|
|
audio codec, release group), text-similarity scoring for
|
|
disambiguating search results (`similarity/`, partial
|
|
`EpisodeMetrics` port), a TMDB metadata client (`web/tmdb.rs` — TV
|
|
search + episode lookup
|
|
and movie search + lookup, both disambiguated via `similarity/`,
|
|
tested against a mocked HTTP server via `mockito`), the on-disk
|
|
rename engine (`rename/` — skip-on-conflict, Move/Copy/Symlink
|
|
actions, can also organize into (auto-created) subfolders when
|
|
the naming scheme includes `/`) — and a rename history/undo log
|
|
(`history/`, a partial `HistoryDialog` port, undo-aware of which
|
|
action produced each batch). 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 for both TV and
|
|
movies, TMDB metadata fetch — `metadata.rs` for TV episodes,
|
|
plus a movie-fetch command alongside it — executing a rename
|
|
plan, native file/folder dialogs, drag-and-drop expansion), plus
|
|
`settings.rs` (TMDB token), `history.rs` (rename history/undo
|
|
log), and `presets.rs` (up to 9 saved naming schemes) — all three
|
|
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 TV/movie
|
|
matching, TMDB fetch for both (behind a Settings modal for the
|
|
API token), a confirm-before-touching-disk modal for the actual
|
|
rename, a History panel that can undo a past rename, Align Up/Down
|
|
list reordering, and a Presets panel (plus `1`-`9` hotkeys) for
|
|
saved naming schemes.
|
|
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
|
|
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`.
|
|
|
|
## 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. A filename-tag-based subset is now ported and wired to "Match"
|
|
(`guess_release_info` in `crates/renamr-core/src/matching`): `{vf}`/`{resolution}`,
|
|
`{source}`, `{vc}`/`{video codec}`, `{ac}`/`{audio codec}`, `{group}`/`{release
|
|
group}`. What's still missing is everything MediaBindingBean derives from actually
|
|
*probing* the media file (real video/audio stream resolution, duration, bitrate,
|
|
frame rate, file size/hash, etc.) — Renamr ships no ffprobe/MediaInfo dependency, so
|
|
those bindings only work when a release name happens to carry the info as text.
|
|
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), and so is a local
|
|
movie title/year heuristic (`guess_movie` in the same module) used as a fallback
|
|
whenever a filename carries no season/episode marker — a release name is either a TV
|
|
episode or a movie, never both. Title 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), shared by both the TV and movie TMDB lookups:
|
|
"Fetch Data" 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 both TV and movies
|
|
(`crates/renamr-core/src/web/tmdb.rs`, `reqwest` + `serde_json`, Bearer-token auth) —
|
|
search-by-name plus per-season episode lookup for TV, search-by-title (optionally
|
|
narrowed by release year via TMDB's own `primary_release_year` filter) for movies —
|
|
both 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. 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. The `copy`/`symlink` `--action` variants are done too — an action
|
|
selector in the confirmation modal picks Move/Copy/Symlink for the whole batch; Copy
|
|
and Symlink both leave the source file untouched and produce a new file/link at the
|
|
computed target, and both are undoable from the History panel the same as Move (undo
|
|
for Copy/Symlink just removes what was created, since there's nothing to move back).
|
|
Not yet done: 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 TV/movie matching, confirmed rename with per-item exclude, rename history/undo,
|
|
Align Up/Down reordering, naming-scheme Presets with `1`-`9` hotkeys) is built; the
|
|
other five sidebar panels (Episodes/Subtitles/SFV/Filter/List) are still stub
|
|
screens. Align Up/Down is button-based (swaps the selected file with its neighbor),
|
|
not the original's drag-and-drop-within-a-list interaction.
|
|
8. **Undo/history for renames** — done, as a flat JSON-backed undo stack rather than a
|
|
full port of `net.filebot.HistoryDialog`'s session/element log
|
|
(`crates/renamr-core/src/history`, `src-tauri/src/history.rs`). Every successful
|
|
rename is recorded to a `history.json` in the OS app-config directory (same location
|
|
as `settings.json`); the History panel lists past batches and can revert one back to
|
|
its original paths, skipping (never overwriting) any file whose original path now
|
|
has something else at it. Capped at the 50 most recent batches.
|
|
9. **Packaging** — code signing, auto-update, installers per platform via
|
|
`npm run tauri build`.
|