Add release-tag naming-scheme fields (resolution, source, codecs, group)

Match now also parses filename-only release-tag text - resolution, source
(BluRay/HDTV/WEB-DL/...), video/audio codec, and release group - via a new
guess_release_info() in renamr-core's matching module, wired through a new
guess_release_info Tauri command and called alongside the existing TV/movie
guess for every file. Naming schemes can now reference
{resolution}/{source}/{video codec}/{audio codec}/{release group}, or
FileBot's own short names ({vf}/{source}/{vc}/{ac}/{group}).
This commit is contained in:
Renamr Scaffold
2026-08-29 09:29:14 +00:00
parent fd4e6f66f0
commit 94eb26f778
4 changed files with 343 additions and 26 deletions
+96 -19
View File
@@ -52,6 +52,19 @@ interface MovieGuess {
// What runMatching resolved a file to, before it's written into the item's bindings.
type MatchGuess = { kind: "tv"; episode: EpisodeGuess } | { kind: "movie"; movie: MovieGuess };
// Result of the local, filename-only release-tag matcher (renamr-core's
// `matching::guess_release_info`, exposed via the `guess_release_info` Tauri command).
// Independent of the TV/movie guess above - a release name carries (or doesn't carry)
// these tags regardless of what kind of media it is, so this runs alongside whichever
// of `guess_episode`/`guess_movie` applied. Every field is optional on its own.
interface ReleaseInfo {
resolution: string | null;
source: string | null;
videoCodec: string | null;
audioCodec: string | null;
group: string | null;
}
function pad2(n: number): string {
return n.toString().padStart(2, "0");
}
@@ -178,6 +191,13 @@ const SAMPLE_ITEMS: Item[] = [
"season number": "5",
"episode number": "14",
"episode name": "Ozymandias",
vf: "720p",
source: "HDTV",
vc: "x264",
group: "KILLERS",
resolution: "720p",
"video codec": "x264",
"release group": "KILLERS",
},
isSample: true,
mediaKind: "tv",
@@ -194,6 +214,13 @@ const SAMPLE_ITEMS: Item[] = [
"season number": "5",
"episode number": "15",
"episode name": "Granite State",
vf: "720p",
source: "HDTV",
vc: "x264",
group: "KILLERS",
resolution: "720p",
"video codec": "x264",
"release group": "KILLERS",
},
isSample: true,
mediaKind: "tv",
@@ -210,6 +237,11 @@ const SAMPLE_ITEMS: Item[] = [
"season number": "3",
"episode number": "01",
"episode name": "Gay Witch Hunt",
source: "DVDRip",
vc: "XviD",
group: "SAiNTS",
"video codec": "XviD",
"release group": "SAiNTS",
},
isSample: true,
mediaKind: "tv",
@@ -483,39 +515,68 @@ export default function RenamePanel() {
return;
}
const results: Array<readonly [string, MatchGuess | null]> = await Promise.all(
targets.map(async (item): Promise<readonly [string, MatchGuess | null]> => {
const results: Array<readonly [string, MatchGuess | null, ReleaseInfo | null]> = await Promise.all(
targets.map(async (item): Promise<readonly [string, MatchGuess | null, ReleaseInfo | null]> => {
// Release tags (resolution/source/codecs/group) are independent of TV-vs-movie,
// so kick that guess off alongside whichever of the two below applies.
const releasePromise = invoke<ReleaseInfo | null>("guess_release_info", {
filename: item.originalName,
}).catch(() => null);
let mediaGuess: MatchGuess | null = null;
try {
const episode = await invoke<EpisodeGuess | null>("guess_episode", {
filename: item.originalName,
});
if (episode) return [item.id, { kind: "tv", episode }];
if (episode) mediaGuess = { kind: "tv", episode };
} catch {
// Fall through and try a movie guess instead.
}
try {
const movie = await invoke<MovieGuess | null>("guess_movie", {
filename: item.originalName,
});
if (movie) return [item.id, { kind: "movie", movie }];
} catch {
// Neither guess worked - leave this item unmatched.
if (!mediaGuess) {
try {
const movie = await invoke<MovieGuess | null>("guess_movie", {
filename: item.originalName,
});
if (movie) mediaGuess = { kind: "movie", movie };
} catch {
// Neither guess worked - leave this item unmatched.
}
}
return [item.id, null];
return [item.id, mediaGuess, await releasePromise];
})
);
const guesses = new Map(results);
const guesses = new Map(results.map(([id, guess]) => [id, guess] as const));
const releaseGuesses = new Map(results.map(([id, , release]) => [id, release] as const));
// Count matches from the results directly, not inside the setItems updater below:
// React doesn't run a functional updater synchronously, so a counter mutated in
// there wouldn't be ready yet for the status message/auto-upgrade check right after.
const matchedTv = results.filter(([, guess]) => guess?.kind === "tv").length;
const matchedMovie = results.filter(([, guess]) => guess?.kind === "movie").length;
const matched = matchedTv + matchedMovie;
const tagged = results.filter(([, , release]) => release !== null).length;
setItems((prev) =>
prev.map((item) => {
const guess = guesses.get(item.id);
if (!guess) return item;
const release = releaseGuesses.get(item.id);
// Release-tag fields apply on top of whatever the TV/movie guess below
// contributes (or nothing at all, for a file with no season/episode or movie
// title recognized but a quality tag in its name).
const releaseBindings: Record<string, string> | null = release
? {
...(release.resolution ? { vf: release.resolution, resolution: release.resolution } : {}),
...(release.source ? { source: release.source } : {}),
...(release.videoCodec ? { vc: release.videoCodec, "video codec": release.videoCodec } : {}),
...(release.audioCodec ? { ac: release.audioCodec, "audio codec": release.audioCodec } : {}),
...(release.group ? { group: release.group, "release group": release.group } : {}),
}
: null;
if (!guess) {
return releaseBindings ? { ...item, bindings: { ...item.bindings, ...releaseBindings } } : item;
}
if (guess.kind === "tv") {
const { episode } = guess;
@@ -534,6 +595,7 @@ export default function RenamePanel() {
"show name": seriesName,
"season number": String(episode.season),
"episode number": pad2(episode.episode),
...releaseBindings,
},
};
}
@@ -551,6 +613,7 @@ export default function RenamePanel() {
// Human-readable aliases.
"movie name": title,
...(movie.year ? { year: String(movie.year) } : {}),
...releaseBindings,
},
};
})
@@ -566,7 +629,7 @@ export default function RenamePanel() {
setStatus(
matched > 0
? `Matched ${matched} of ${targets.length} file${targets.length === 1 ? "" : "s"} (${matchedTv} TV, ${matchedMovie} movie${matchedMovie === 1 ? "" : "s"}; filename-based, local only).`
? `Matched ${matched} of ${targets.length} file${targets.length === 1 ? "" : "s"} (${matchedTv} TV, ${matchedMovie} movie${matchedMovie === 1 ? "" : "s"}${tagged > 0 ? `, ${tagged} with release tags` : ""}; filename-based, local only).`
: "No season/episode or movie title recognized in the loaded filenames."
);
window.clearTimeout((runMatching as any)._t);
@@ -1094,13 +1157,27 @@ export default function RenamePanel() {
<code>{"{year}"}</code>
<code>{"{airdate}"}</code>
</div>
<div className="format-fields-group">
<span className="format-fields-group-title">
Release tags, after <strong>Match</strong> (TV or movies, local, offline)
</span>
<code>{"{resolution}"}</code>
<code>{"{source}"}</code>
<code>{"{video codec}"}</code>
<code>{"{audio codec}"}</code>
<code>{"{release group}"}</code>
</div>
</div>
<p className="format-hint">
Match scans filenames locally for season/episode or movie title/year patterns (no network calls); Fetch
Data resolves that guess against real TMDB metadata for the real title, episode title, and air/release
date — needs a free TMDB API token set in <strong>TMDB Settings</strong> first. A batch can freely mix TV
and movie files — each file only fills in the fields that apply to it. FileBot's own short field names
(<code>{"{n}"}</code>, <code>{"{s00e00}"}</code>, <code>{"{t}"}</code>, <code>{"{y}"}</code>, etc.) still
Match scans filenames locally for season/episode or movie title/year patterns, plus release-tag text
like resolution, source (BluRay/HDTV/WEB-DL/…), video/audio codec, and release group — none of this
needs a network call, since it's all parsed straight from the filename itself (Renamr has no video/audio
file prober, so a tag that isn't in the filename text just won't be available). Fetch Data resolves the
season/episode or movie guess against real TMDB metadata for the real title, episode title, and
air/release date — needs a free TMDB API token set in <strong>TMDB Settings</strong> first. A batch can
freely mix TV and movie files — each file only fills in the fields that apply to it. FileBot's own short
field names (<code>{"{n}"}</code>, <code>{"{s00e00}"}</code>, <code>{"{t}"}</code>, <code>{"{y}"}</code>,
<code>{"{vf}"}</code>, <code>{"{vc}"}</code>, <code>{"{ac}"}</code>, <code>{"{group}"}</code>, etc.) still
work too, side by side with the names above.
</p>
<p className="format-hint">