Wire real media probing into Filter and List panels

Extends the sixteenth delivery's container-header probing
(probe_media_files) beyond the Rename panel: Filter and List now run the
same batched probe alongside their existing local scan, using the same
override rule everywhere - a filename tag wins when present, otherwise a
real probed value fills in.

List: {width}/{height}/{fps}/{duration}/{file size} are now available in
naming-scheme-style expressions (no filename-tag equivalent exists), and a
probed {resolution}/{video codec}/{audio codec} fills in when the filename
didn't already carry that tag.

Filter: gained Resolution and Video codec chip rows (populated from the
same merged filename-tag-or-probed value, with an Unknown bucket for files
with neither) so a batch can be narrowed by either. Each result row's
subtitle now also shows the resolved resolution when one exists.

Verified via cargo check/test --workspace (unaffected, frontend-only
change) + npm run build (clean) + a 15-check Playwright script against
npm run dev covering both panels: filename-tag resolution winning over a
conflicting probed value, probed fields filling in for an otherwise-
unmatched file, the new Filter chip rows narrowing results correctly.
Updated README (status paragraph, Filter section, List section, Real
media-file probing section, roadmap item 1).
This commit is contained in:
Renamr Scaffold
2026-08-30 03:48:22 +00:00
parent 23c9ca609f
commit e219e8f2a8
3 changed files with 283 additions and 58 deletions
+66 -44
View File
@@ -64,7 +64,9 @@ file via a native Save dialog — this closes out the full original panel set (R
Episodes/Subtitles/SFV/Filter/List); see "List" below. Real media-file probing (real
resolution, frame rate, duration, and codecs read straight from a video file's own
container header, not guessed from its filename) is wired into the Rename panel's
"Match" step — see "Real media-file probing" below. The naming-expression engine's Rhai
"Match" step, and now the Filter and List panels' own local scans too — Filter gained
Resolution/Video codec filter chips from it, and List's expressions can reference the
same probed fields — see "Real media-file probing" below. The naming-expression engine's Rhai
scripting layer also gained a batch of FileBot-style string helpers — `.pad()`,
`.sort_name()`/`.sort_initial()`, `.upper_initial()`/`.lower_initial()`, `.acronym()`,
`.validate_file_name()`, `.space()`/`.space_with()`, `.colon()`/`.colon_with()` — callable
@@ -500,21 +502,29 @@ write hex digests in uppercase and others in lowercase.
The Filter sidebar panel loads a batch of files (independent of whatever's loaded in the
Rename panel) and narrows it down before you decide what to do with it elsewhere — copy
the matching paths out, or just get a sense of what's actually in a folder. Unlike SFV,
it adds no new Rust/Tauri code at all: it scans every loaded file with the exact same
local, filename-only matchers "Match" uses (`guess_episode`/`guess_movie`/
`guess_release_info`) and turns the results into filter criteria instead of a rename
preview — no network calls, so scanning even a large batch is effectively instant.
the matching paths out, or just get a sense of what's actually in a folder. It scans every
loaded file with the exact same local, filename-only matchers "Match" uses
(`guess_episode`/`guess_movie`/`guess_release_info`) and turns the results into filter
criteria instead of a rename preview — no network calls, so scanning even a large batch is
effectively instant. It also runs the same batched `probe_media_files` real container-header
probe "Match" and List do (see "Real media-file probing" below), so a file's resolution/
video codec can come from real probing even when nothing in its filename says so.
**Load Files…** loads and scans a batch in one step. Toggleable chips then appear for
every **extension** actually present (e.g. MKV/MP4/TXT) and every **kind** Aurora Curator
locally recognized (TV/Movies/Unmatched) — click a chip to exclude that group, click it
again to bring it back. A **filename contains** field does a live, case-insensitive
again to bring it back. **Resolution** and **Video codec** chip rows appear the same way
whenever at least one loaded file actually has a value for either (a filename tag wins
when present, otherwise a real probed value fills in — same override rule as everywhere
else this delivery touches; files with neither fall into their own "Unknown" chip rather
than being silently excluded). A **filename contains** field does a live, case-insensitive
substring search, and a **season** field (shown only while the TV chip is enabled) narrows
to a single season number, automatically excluding anything that isn't a TV episode in
that season. The results list updates live with an "N of M files match" count, and
**Copy File Paths** writes every currently-matching file's full path, one per line, to the
clipboard.
that season. Each result row's subtitle also shows a resolved resolution when one exists
(e.g. `Unmatched · 480p` for a file Match couldn't identify as TV or a movie but still got
a real probed resolution for). The results list updates live with an "N of M files match"
count, and **Copy File Paths** writes every currently-matching file's full path, one per
line, to the clipboard.
## List
@@ -530,26 +540,31 @@ native Save dialog and writes whatever text the frontend hands it to a `.txt` fi
everything else (matching, and rendering each file's line) reuses commands that already
existed.
**Load Files…** loads a batch and scans it locally, the same way Filter does. The
**Format** field holds the expression rendered against each file — the exact same `{ }`
syntax the naming scheme uses, run through the same `render_expression` command the
Rename panel's live preview calls. `{name}`/`{ext}` always work (they're the file's own
name/extension); `{show name}`/`{season number}`/`{episode number}`, `{movie name}`/
`{year}`, and the release-tag fields (`{resolution}`/`{source}`/`{video codec}`/
`{audio codec}`/`{release group}`) fill in once the local scan recognizes a file — there's
no Fetch Data step here, List stays local-only like Filter. A file whose rendered line
comes out empty (e.g. an expression that only references a field this particular file
never matched) shows an "(empty)" placeholder rather than blocking the rest of the list,
and the results header tracks "N of M lines rendered" live as the expression changes.
**Copy List** copies every line, in file order, newline-joined, to the clipboard; **Export
to File…** opens a native Save dialog (defaulted to `list.txt`) and writes the same text
to disk.
**Load Files…** loads a batch and scans it locally, the same way Filter does — including
the same batched real container-header probe (see "Real media-file probing" below), so
List's expressions get real probed values too, not just filename-tag ones. The **Format**
field holds the expression rendered against each file — the exact same `{ }` syntax the
naming scheme uses, run through the same `render_expression` command the Rename panel's
live preview calls. `{name}`/`{ext}` always work (they're the file's own name/extension);
`{show name}`/`{season number}`/`{episode number}`, `{movie name}`/`{year}`, and the
release-tag fields (`{resolution}`/`{source}`/`{video codec}`/`{audio codec}`/`{release
group}`) fill in once the local scan recognizes a file; `{width}`/`{height}`/`{fps}`/
`{duration}`/`{file size}` come straight from real probing (no filename-tag equivalent
exists), and a probed `{resolution}`/`{video codec}`/`{audio codec}` fills in whenever the
filename didn't already carry that tag — there's no Fetch Data step here, List stays
local-only like Filter. A file whose rendered line comes out empty (e.g. an expression
that only references a field this particular file never matched) shows an "(empty)"
placeholder rather than blocking the rest of the list, and the results header tracks
"N of M lines rendered" live as the expression changes. **Copy List** copies every line, in
file order, newline-joined, to the clipboard; **Export to File…** opens a native Save
dialog (defaulted to `list.txt`) and writes the same text to disk.
## Real media-file probing
Unlike every panel above, this isn't a new sidebar screen — it's a naming-scheme
binding source wired into the Rename panel's existing "Match" step, closing (part of)
the gap the porting roadmap has called out since the beginning: FileBot's
binding source wired into the Rename panel's "Match" step, the Filter panel's scan, and
the List panel's scan alike, closing (part of) the gap the porting roadmap has called out
since the beginning: FileBot's
`MediaBindingBean` doesn't just parse tags out of a release name, it also reads a
video file's own container header for its real resolution, frame rate, duration, and
codecs — a naming scheme can reference those even when the filename itself doesn't
@@ -562,19 +577,24 @@ values — probing is always a best-effort enrichment layered on top of filename
matching, never a hard requirement, so an unsupported or corrupt file can't break
Match.
Clicking **Match** now also probes every loaded file's real container header
(`probe_media_files`, one batched Tauri round trip off the main thread) alongside the
existing filename-based guessing. The result feeds the same bindings the release-tag
parser already populates — `{resolution}`/`{vf}`, `{video codec}`/`{vc}`,
`{audio codec}`/`{ac}` — but only when the filename itself didn't already carry that
tag; an explicit release-group tag always wins over a probed value, since a release
group's own tag is usually deliberate. Five bindings have no filename-tag equivalent
at all, so they're only ever available this way: `{width}`, `{height}`, `{fps}`,
`{duration}` (`h:mm:ss`, or `m:ss` under an hour), and `{file size}` (human-readable,
e.g. `1.4 GB`) — all formatted once on the Rust side so the UI never reimplements that
logic. The Match status line reports how many loaded files were actually probed
(`"N probed from file"`), and the naming-scheme editor's field-reference list has a
new "Real file probing" group listing all five.
Clicking **Match** (Rename), or loading a batch into **Filter** or **List**, now also
probes every loaded file's real container header (`probe_media_files`, one batched Tauri
round trip off the main thread) alongside the existing filename-based guessing — all
three panels call the exact same command the exact same way. The result feeds the same
bindings the release-tag parser already populates — `{resolution}`/`{vf}`, `{video
codec}`/`{vc}`, `{audio codec}`/`{ac}` — but only when the filename itself didn't already
carry that tag; an explicit release-group tag always wins over a probed value, since a
release group's own tag is usually deliberate. Five bindings have no filename-tag
equivalent at all, so they're only ever available this way: `{width}`, `{height}`,
`{fps}`, `{duration}` (`h:mm:ss`, or `m:ss` under an hour), and `{file size}`
(human-readable, e.g. `1.4 GB`) — all formatted once on the Rust side so the UI never
reimplements that logic. Rename's Match status line, List's scan status, and Filter's
scan status all report how many loaded files were actually probed (`"N probed from
file"`), the naming-scheme editor's field-reference list has a "Real file probing" group
listing all five width/height/fps/duration/file-size bindings, and the Filter panel adds
its own **Resolution**/**Video codec** chip rows (populated from the same merged
filename-tag-or-probed value) so a batch can be narrowed by either, not just by
extension/kind/name/season.
## Porting roadmap (not yet done)
@@ -591,11 +611,13 @@ ported from, for anyone picking this up:
actually reading the file — real resolution/video codec/audio codec fill in
whenever the filename didn't already carry that tag, plus `{width}`/`{height}`/
`{fps}`/`{duration}`/`{file size}`, which have no filename-tag equivalent at all —
for the two container families it supports (MP4/M4V/MOV and MKV/WebM). Still
missing: probing for other containers (.avi, .wmv, ...), audio-only metadata
(bitrate, sample rate, channel layout), file hashing (`{crc32}` reuses the SFV
panel's own hasher today but isn't wired into naming-scheme bindings yet), and
wiring probed bindings into the List/Filter panels (currently Rename-panel-only).
for the two container families it supports (MP4/M4V/MOV and MKV/WebM). Probed
bindings are now wired into all three panels that do local matching — Rename's
Match step, and the Filter and List panels' own scans (Filter also gained
Resolution/Video codec filter chips from it). Still missing: probing for other
containers (.avi, .wmv, ...), audio-only metadata (bitrate, sample rate, channel
layout), and file hashing (`{crc32}` reuses the SFV panel's own hasher today but
isn't wired into naming-scheme bindings yet).
2. **String/expression helper methods** — `net.filebot.format.ExpressionFormatMethods`
(636 lines): FileBot-specific Groovy string extensions like `.pad()`, `.sortName()`.
Ported so far (`crates/aurora-curator-core/src/format/functions.rs`): `any`, `all_of`,
+146 -7
View File
@@ -29,6 +29,32 @@ interface ReleaseInfo {
group: string | null;
}
// Result of real container-header probing (aurora-curator-core's `probe` module, exposed
// via the batched `probe_media_files` Tauri command) - resolution/codecs read straight
// from the file's own header, not guessed from its name. Complements (never overrides)
// `ReleaseInfo` above, same as the Rename and List panels: a filename-tag value wins
// whenever a file has both.
interface MediaProbe {
fileSizeBytes: number;
durationSeconds: number | null;
width: number | null;
height: number | null;
frameRate: number | null;
videoCodec: string | null;
audioCodec: string | null;
container: string | null;
resolution: string | null;
duration: string | null;
fileSize: string;
}
interface ProbeOutcome {
path: string;
probe: MediaProbe;
}
const UNKNOWN = "(unknown)";
type MediaKind = "tv" | "movie" | "unmatched";
interface FileInfo {
@@ -39,6 +65,12 @@ interface FileInfo {
movieName?: string;
year?: number | null;
release: ReleaseInfo | null;
// Merged resolution/video codec - a filename tag wins when present, otherwise falls
// back to real container probing. Every file gets a chance at both, including an
// otherwise-unmatched one (see `scan` below).
resolution: string | null;
videoCodec: string | null;
probed: boolean;
}
const KIND_LABELS: Record<MediaKind, string> = {
@@ -57,14 +89,15 @@ function displayName(file: FileEntry): string {
function subtitleFor(info: FileInfo | undefined): string {
if (!info) return "";
const tag = info.resolution ? ` · ${info.resolution}` : "";
if (info.kind === "tv") {
const se = `S${String(info.season ?? 0).padStart(2, "0")}E${String(info.episode ?? 0).padStart(2, "0")}`;
return info.seriesName ? `${info.seriesName} · ${se}` : se;
return (info.seriesName ? `${info.seriesName} · ${se}` : se) + tag;
}
if (info.kind === "movie") {
return info.year ? `${info.movieName} (${info.year})` : info.movieName ?? "";
return (info.year ? `${info.movieName} (${info.year})` : info.movieName ?? "") + tag;
}
return "Unmatched";
return "Unmatched" + tag;
}
export default function FilterPanel() {
@@ -73,6 +106,8 @@ export default function FilterPanel() {
const [scanning, setScanning] = useState(false);
const [extensions, setExtensions] = useState<Set<string>>(new Set());
const [kinds, setKinds] = useState<Set<MediaKind>>(new Set());
const [resolutions, setResolutions] = useState<Set<string>>(new Set());
const [videoCodecs, setVideoCodecs] = useState<Set<string>>(new Set());
const [nameContains, setNameContains] = useState("");
const [season, setSeason] = useState("");
const [status, setStatus] = useState<string | null>(null);
@@ -91,6 +126,13 @@ export default function FilterPanel() {
async function scan(targets: FileEntry[]) {
setScanning(true);
try {
// One batched round trip for real probing of every target, run alongside the
// per-file filename guessing below rather than after it - same pattern as the
// Rename and List panels.
const probePromise = invoke<ProbeOutcome[]>("probe_media_files", {
paths: targets.map((file) => file.path),
}).catch(() => [] as ProbeOutcome[]);
const results = await Promise.all(
targets.map(async (file): Promise<readonly [string, FileInfo]> => {
const releasePromise = invoke<ReleaseInfo | null>("guess_release_info", {
@@ -107,6 +149,9 @@ export default function FilterPanel() {
season: episode.season,
episode: episode.episode,
release: null,
resolution: null,
videoCodec: null,
probed: false,
};
}
} catch {
@@ -116,31 +161,61 @@ export default function FilterPanel() {
try {
const movie = await invoke<MovieGuess | null>("guess_movie", { filename: displayName(file) });
if (movie) {
fileInfo = { kind: "movie", movieName: movie.title, year: movie.year, release: null };
fileInfo = {
kind: "movie",
movieName: movie.title,
year: movie.year,
release: null,
resolution: null,
videoCodec: null,
probed: false,
};
}
} catch {
// Neither guess worked.
}
}
if (!fileInfo) fileInfo = { kind: "unmatched", release: null };
if (!fileInfo) {
fileInfo = { kind: "unmatched", release: null, resolution: null, videoCodec: null, probed: false };
}
fileInfo.release = await releasePromise;
return [file.path, fileInfo] as const;
})
);
const probes = new Map((await probePromise).map((outcome) => [outcome.path, outcome.probe] as const));
const nextInfo: Record<string, FileInfo> = {};
const seenExt = new Set<string>();
const seenKinds = new Set<MediaKind>();
const seenRes = new Set<string>();
const seenCodec = new Set<string>();
let probedCount = 0;
for (const [path, fileInfo] of results) {
const probe = probes.get(path);
// Real-probed resolution/video codec only fill in when the filename tag
// above didn't already provide them - same override rule the Rename and List
// panels use. A file with neither still gets a chance at both, even one
// Match couldn't recognize as TV or a movie at all.
fileInfo.resolution = fileInfo.release?.resolution ?? probe?.resolution ?? null;
fileInfo.videoCodec = fileInfo.release?.videoCodec ?? probe?.videoCodec ?? null;
fileInfo.probed = probe?.container != null;
if (fileInfo.probed) probedCount += 1;
nextInfo[path] = fileInfo;
seenKinds.add(fileInfo.kind);
seenRes.add(fileInfo.resolution ?? UNKNOWN);
seenCodec.add(fileInfo.videoCodec ?? UNKNOWN);
}
for (const file of targets) seenExt.add(file.ext);
setInfo(nextInfo);
setExtensions(seenExt);
setKinds(seenKinds);
setStatus(`Scanned ${targets.length} file${targets.length === 1 ? "" : "s"} locally (no network calls).`);
setResolutions(seenRes);
setVideoCodecs(seenCodec);
setStatus(
`Scanned ${targets.length} file${targets.length === 1 ? "" : "s"} locally (${probedCount > 0 ? `${probedCount} probed from file; ` : ""}no network calls).`
);
} finally {
setScanning(false);
}
@@ -168,10 +243,33 @@ export default function FilterPanel() {
});
}
function toggleResolution(res: string) {
setResolutions((prev) => {
const next = new Set(prev);
if (next.has(res)) next.delete(res);
else next.add(res);
return next;
});
}
function toggleVideoCodec(codec: string) {
setVideoCodecs((prev) => {
const next = new Set(prev);
if (next.has(codec)) next.delete(codec);
else next.add(codec);
return next;
});
}
const allExtensions = Array.from(new Set(files.map((f) => f.ext))).sort();
const allKinds = (["tv", "movie", "unmatched"] as MediaKind[]).filter((k) =>
Object.values(info).some((i) => i.kind === k)
);
// Real container probing is best-effort, so these rows only show up once something
// actually has a real value - a batch of files with no recognizable resolution/codec
// at all (e.g. non-media files) doesn't get a useless "Unknown"-only row.
const allResolutions = Array.from(new Set(Object.values(info).map((i) => i.resolution ?? UNKNOWN))).sort();
const allVideoCodecs = Array.from(new Set(Object.values(info).map((i) => i.videoCodec ?? UNKNOWN))).sort();
const seasonFilter = kinds.has("tv") && season.trim() !== "" ? Number(season) : null;
@@ -180,6 +278,8 @@ export default function FilterPanel() {
if (!fileInfo) return false;
if (!extensions.has(file.ext)) return false;
if (!kinds.has(fileInfo.kind)) return false;
if (!resolutions.has(fileInfo.resolution ?? UNKNOWN)) return false;
if (!videoCodecs.has(fileInfo.videoCodec ?? UNKNOWN)) return false;
if (nameContains.trim() && !displayName(file).toLowerCase().includes(nameContains.trim().toLowerCase())) {
return false;
}
@@ -244,6 +344,40 @@ export default function FilterPanel() {
</div>
)}
{allResolutions.some((r) => r !== UNKNOWN) && (
<div className="filter-criteria-row">
<span className="filter-criteria-label">Resolution</span>
<div className="filter-chip-row" role="group" aria-label="Filter by resolution">
{allResolutions.map((res) => (
<button
key={res}
className={`filter-chip${resolutions.has(res) ? " selected" : ""}`}
onClick={() => toggleResolution(res)}
>
{res === UNKNOWN ? "Unknown" : res}
</button>
))}
</div>
</div>
)}
{allVideoCodecs.some((c) => c !== UNKNOWN) && (
<div className="filter-criteria-row">
<span className="filter-criteria-label">Video codec</span>
<div className="filter-chip-row" role="group" aria-label="Filter by video codec">
{allVideoCodecs.map((codec) => (
<button
key={codec}
className={`filter-chip${videoCodecs.has(codec) ? " selected" : ""}`}
onClick={() => toggleVideoCodec(codec)}
>
{codec === UNKNOWN ? "Unknown" : codec}
</button>
))}
</div>
</div>
)}
<div className="filter-criteria-row">
<span className="filter-criteria-label">Filename contains</span>
<input
@@ -272,7 +406,12 @@ export default function FilterPanel() {
)}
<div className="filter-results">
{files.length === 0 && <div className="filter-empty-note">Load files to filter them by extension, kind, name, or season.</div>}
{files.length === 0 && (
<div className="filter-empty-note">
Load files to filter them by extension, kind, name, season, or (when real container probing or a
filename tag finds one) resolution and video codec.
</div>
)}
{files.length > 0 && (
<div className="filter-results-header">
+71 -7
View File
@@ -29,6 +29,31 @@ interface ReleaseInfo {
group: string | null;
}
// Result of real container-header probing (aurora-curator-core's `probe` module, exposed
// via the batched `probe_media_files` Tauri command) - resolution/duration/codecs read
// straight from the file's own header, not guessed from its name. Complements (never
// overrides) `ReleaseInfo` above, same as the Rename panel's Match step: a filename-tag
// value wins whenever a file has both. `resolution`/`duration`/`fileSize` are
// pre-formatted display strings computed on the Rust side.
interface MediaProbe {
fileSizeBytes: number;
durationSeconds: number | null;
width: number | null;
height: number | null;
frameRate: number | null;
videoCodec: string | null;
audioCodec: string | null;
container: string | null;
resolution: string | null;
duration: string | null;
fileSize: string;
}
interface ProbeOutcome {
path: string;
probe: MediaProbe;
}
// The naming-scheme-style expression List renders per file, reusing exactly the same
// `render_expression` command (and therefore the same `{ }` syntax) the Rename panel's
// live preview uses. `{name}`/`{ext}` always work; every other field only fills in once
@@ -68,8 +93,15 @@ export default function ListPanel() {
async function scan(targets: FileEntry[], base: Record<string, Record<string, string>>) {
setScanning(true);
try {
// One batched round trip for real probing of every target, run alongside the
// per-file filename guessing below rather than after it - same pattern as the
// Rename panel's Match step.
const probePromise = invoke<ProbeOutcome[]>("probe_media_files", {
paths: targets.map((file) => file.path),
}).catch(() => [] as ProbeOutcome[]);
const results = await Promise.all(
targets.map(async (file): Promise<readonly [string, Record<string, string>]> => {
targets.map(async (file): Promise<readonly [string, Record<string, string>, ReleaseInfo | null]> => {
const releasePromise = invoke<ReleaseInfo | null>("guess_release_info", {
filename: displayName(file),
}).catch(() => null);
@@ -120,19 +152,47 @@ export default function ListPanel() {
}
: {};
return [file.path, { ...base[file.path], ...extra, ...releaseBindings }] as const;
return [file.path, { ...base[file.path], ...extra, ...releaseBindings }, release] as const;
})
);
const probes = new Map((await probePromise).map((outcome) => [outcome.path, outcome.probe] as const));
const next: Record<string, Record<string, string>> = {};
let matchedCount = 0;
for (const [path, b] of results) {
next[path] = b;
if (b.n) matchedCount += 1;
let probedCount = 0;
for (const [path, b, release] of results) {
const probe = probes.get(path);
// Real-probed fields: `resolution`/`video codec`/`audio codec` only fill in
// when the filename tag above didn't already provide that field, but
// `width`/`height`/`fps`/`duration`/`file size` have no filename-tag
// equivalent at all, so those always apply whenever probing found them - same
// override rule the Rename panel's Match step uses.
const probeBindings: Record<string, string> = probe
? {
"file size": probe.fileSize,
...(probe.width ? { width: String(probe.width) } : {}),
...(probe.height ? { height: String(probe.height) } : {}),
...(probe.frameRate ? { fps: String(Math.round(probe.frameRate * 100) / 100) } : {}),
...(probe.duration ? { duration: probe.duration } : {}),
...(!release?.resolution && probe.resolution
? { vf: probe.resolution, resolution: probe.resolution }
: {}),
...(!release?.videoCodec && probe.videoCodec
? { vc: probe.videoCodec, "video codec": probe.videoCodec }
: {}),
...(!release?.audioCodec && probe.audioCodec
? { ac: probe.audioCodec, "audio codec": probe.audioCodec }
: {}),
}
: {};
if (probe?.container) probedCount += 1;
next[path] = { ...b, ...probeBindings };
if (next[path].n) matchedCount += 1;
}
setBindings(next);
setStatus(
`Scanned ${targets.length} file${targets.length === 1 ? "" : "s"} locally (${matchedCount} matched; no network calls).`
`Scanned ${targets.length} file${targets.length === 1 ? "" : "s"} locally (${matchedCount} matched${probedCount > 0 ? `, ${probedCount} probed from file` : ""}; no network calls).`
);
} finally {
setScanning(false);
@@ -225,7 +285,11 @@ export default function ListPanel() {
Same <code>{"{ }"}</code> syntax as the naming scheme. <code>{"{name}"}</code>/<code>{"{ext}"}</code> always work;{" "}
<code>{"{show name}"}</code>/<code>{"{season number}"}</code>/<code>{"{episode number}"}</code>,{" "}
<code>{"{movie name}"}</code>/<code>{"{year}"}</code>, and release tags fill in once a file is recognized -
local matching only, no Fetch Data step.
local matching only, no Fetch Data step. Files also get real container probing the same as the Rename
panel's Match step: <code>{"{width}"}</code>/<code>{"{height}"}</code>/<code>{"{fps}"}</code>/
<code>{"{duration}"}</code>/<code>{"{file size}"}</code> come straight from the file's own header, and a
probed <code>{"{resolution}"}</code>/<code>{"{video codec}"}</code>/<code>{"{audio codec}"}</code> fills in
whenever the filename itself didn't already carry that tag.
</div>
<div className="list-results">