Add OMDb as a datasource: TV episodes (third option alongside TMDB/TVmaze) and movies (new second option alongside TMDB)

Movies get their own independent metadata-source setting for the first
time (previously hardcoded to TMDB) so the GUI's Fetch Data Settings now
has two separate pickers - TV episode source (TMDB/TVmaze/OMDb) and
movie source (TMDB/OMDb) - and a batch can freely mix sources across its
TV and movie halves. The -rename CLI's --db flag gained omdb alongside
tmdb/tvmaze, with movies now looked up online for both tmdb and omdb.
This commit is contained in:
Renamr Scaffold
2026-08-29 12:25:45 +00:00
parent 22a3da9f6f
commit c158befa83
11 changed files with 1234 additions and 174 deletions
+8
View File
@@ -530,6 +530,14 @@
font-size: 12.5px;
}
.modal-inline-remove {
width: auto;
align-self: flex-start;
margin: 6px 0 4px;
padding: 4px 12px;
font-size: 11.5px;
}
.history-batch-list {
gap: 6px;
}
+185 -50
View File
@@ -106,9 +106,15 @@ type ConflictPolicy = "skip" | "autoNumber" | "override";
// Mirrors aurora-curator-core's `TvMetadataSource` - which online source Fetch Data
// resolves TV episodes against. "tmdb" is the default (matches Aurora Curator's original and
// only source before TVmaze existed); "tvmaze" needs no API token at all. Movies always
// use TMDB regardless of this setting - TVmaze has no movie catalog to pick between.
type TvMetadataSource = "tmdb" | "tvmaze";
// only source before TVmaze existed); "tvmaze" needs no API token at all; "omdb" needs
// its own separate API key (see MovieMetadataSource below - the two settings are
// independent, since OMDb covers both TV and movies but TVmaze only covers TV).
type TvMetadataSource = "tmdb" | "tvmaze" | "omdb";
// Mirrors aurora-curator-core's `MovieMetadataSource` - which online source Fetch Data
// resolves movies against. Unlike TV there's no zero-setup option (TVmaze has no movie
// catalog) - both choices need their own saved credential.
type MovieMetadataSource = "tmdb" | "omdb";
// Sent to `fetch_metadata`: one file's local guess (from Match) to resolve against
// real online TV metadata (TMDB or TVmaze, per the stored TvMetadataSource). Needs a
@@ -324,12 +330,29 @@ const CONFLICT_POLICY_LABELS: Record<ConflictPolicy, { button: string; descripti
const TV_SOURCE_LABELS: Record<TvMetadataSource, { button: string; description: string }> = {
tmdb: {
button: "TMDB",
description: "Uses TheMovieDB for TV episode titles and air dates — needs the API token below.",
description: "Uses TheMovieDB for TV episode titles and air dates — needs the TMDB API token below.",
},
tvmaze: {
button: "TVmaze",
description: "Uses TVmaze for TV episode titles and air dates — free, no API token needed at all.",
},
omdb: {
button: "OMDb",
description: "Uses OMDb for TV episode titles and air dates — needs the OMDb API key below.",
},
};
// Copy for each MovieMetadataSource, used in the Fetch Data Settings modal's own
// movie-source segmented selector and its description note. Mirrors TV_SOURCE_LABELS.
const MOVIE_SOURCE_LABELS: Record<MovieMetadataSource, { button: string; description: string }> = {
tmdb: {
button: "TMDB",
description: "Uses TheMovieDB for movie titles and release dates — needs the TMDB API token below.",
},
omdb: {
button: "OMDb",
description: "Uses OMDb for movie titles and release dates — needs the OMDb API key below.",
},
};
function realFileToItem(entry: FileEntry): Item {
@@ -425,12 +448,19 @@ export default function RenamePanel() {
// nothing saved, `true` = a token is on file (the value itself never comes back to
// the frontend after saving - see Settings modal below).
const [hasTmdbToken, setHasTmdbToken] = useState<boolean | null>(null);
// Same idea as hasTmdbToken, but for the OMDb API key - the two credentials are
// entirely independent, since either source can be picked for TV, movies, or both.
const [hasOmdbKey, setHasOmdbKey] = useState<boolean | null>(null);
// Which online source Fetch Data uses for TV episodes - persisted on disk (see
// `src-tauri/src/settings.rs`), defaults to "tmdb" until loaded. Movies always use
// TMDB regardless of this, so it only ever gates the TV half of Fetch Data.
// `src-tauri/src/settings.rs`), defaults to "tmdb" until loaded.
const [tvSource, setTvSource] = useState<TvMetadataSource>("tmdb");
// Which online source Fetch Data uses for movies - persisted separately from
// tvSource (see `src-tauri/src/settings.rs`'s MovieMetadataSource), defaults to
// "tmdb" until loaded. TVmaze isn't an option here - it has no movie catalog.
const [movieSource, setMovieSource] = useState<MovieMetadataSource>("tmdb");
const [settingsOpen, setSettingsOpen] = useState(false);
const [tmdbTokenInput, setTmdbTokenInput] = useState("");
const [omdbKeyInput, setOmdbKeyInput] = useState("");
const [savingSettings, setSavingSettings] = useState(false);
const [fetching, setFetching] = useState(false);
// Rename history / undo. `null` batches = not loaded yet (also used to show a
@@ -462,12 +492,24 @@ export default function RenamePanel() {
.catch(() => setHasTmdbToken(false));
}, []);
useEffect(() => {
invoke<string | null>("get_omdb_api_key")
.then((key) => setHasOmdbKey(!!key))
.catch(() => setHasOmdbKey(false));
}, []);
useEffect(() => {
invoke<TvMetadataSource>("get_tv_metadata_source")
.then(setTvSource)
.catch(() => setTvSource("tmdb"));
}, []);
useEffect(() => {
invoke<MovieMetadataSource>("get_movie_metadata_source")
.then(setMovieSource)
.catch(() => setMovieSource("tmdb"));
}, []);
// Refs so the drag-and-drop listener (registered once on mount) always sees the
// latest expression/items instead of the stale values from its first render.
const expressionRef = useRef(expression);
@@ -907,27 +949,40 @@ export default function RenamePanel() {
const openSettings = () => {
setTmdbTokenInput("");
setOmdbKeyInput("");
setSettingsOpen(true);
};
const saveTmdbToken = async () => {
const trimmed = tmdbTokenInput.trim();
if (!trimmed) {
// Saves whichever credential field(s) the user actually typed into, then closes the
// modal - a single Save button covers both the TMDB token and the OMDb key so
// filling in one (or both) at once only takes one click, mirroring how the two
// segmented source pickers above it already sit side by side in the same modal.
const saveCredentials = async () => {
const trimmedTmdb = tmdbTokenInput.trim();
const trimmedOmdb = omdbKeyInput.trim();
if (!trimmedTmdb && !trimmedOmdb) {
// Nothing typed - just close, leaving whatever was already saved untouched.
setSettingsOpen(false);
return;
}
setSavingSettings(true);
try {
await invoke("save_tmdb_token", { token: trimmed });
setHasTmdbToken(true);
if (trimmedTmdb) {
await invoke("save_tmdb_token", { token: trimmedTmdb });
setHasTmdbToken(true);
}
if (trimmedOmdb) {
await invoke("save_omdb_api_key", { key: trimmedOmdb });
setHasOmdbKey(true);
}
setTmdbTokenInput("");
setOmdbKeyInput("");
setSettingsOpen(false);
setStatus("TMDB token saved.");
window.clearTimeout((saveTmdbToken as any)._t);
(saveTmdbToken as any)._t = window.setTimeout(() => setStatus(null), 3000);
setStatus("Fetch Data settings saved.");
window.clearTimeout((saveCredentials as any)._t);
(saveCredentials as any)._t = window.setTimeout(() => setStatus(null), 3000);
} catch (err) {
setStatus(`Couldn't save the token: ${err}`);
setStatus(`Couldn't save: ${err}`);
} finally {
setSavingSettings(false);
}
@@ -964,12 +1019,42 @@ export default function RenamePanel() {
}
};
// Same idea as saveTvSource, for the movie-source picker.
const saveMovieSource = async (source: MovieMetadataSource) => {
setMovieSource(source);
try {
await invoke("save_movie_metadata_source", { source });
} catch (err) {
setStatus(`Couldn't save the movie source setting: ${err}`);
window.clearTimeout((saveMovieSource as any)._t);
(saveMovieSource as any)._t = window.setTimeout(() => setStatus(null), 4000);
}
};
const clearOmdbApiKey = async () => {
setSavingSettings(true);
try {
await invoke("save_omdb_api_key", { key: "" });
setHasOmdbKey(false);
setOmdbKeyInput("");
setStatus("OMDb API key removed.");
window.clearTimeout((clearOmdbApiKey as any)._t);
(clearOmdbApiKey as any)._t = window.setTimeout(() => setStatus(null), 3000);
} catch (err) {
setStatus(`Couldn't remove the key: ${err}`);
} finally {
setSavingSettings(false);
}
};
// "Fetch Data": resolve each already-Matched file's local guess against real online
// metadata. Requires Match to have already populated either a series name + season/
// episode (TV) or a movie title (movie) - that's the search query - so this is
// deliberately layered on top of local matching, not a replacement for it. TV items
// resolve against whichever source `tvSource` currently picks (TMDB or TVmaze); movie
// items always resolve against TMDB. Both run via separate Tauri commands,
// resolve against whichever source `tvSource` currently picks (TMDB, TVmaze, or
// OMDb); movie items resolve against whichever source `movieSource` picks (TMDB or
// OMDb) - the two settings are independent, so a batch mixing TV and movies can even
// use different sources for each half. Both run via separate Tauri commands,
// concurrently, and merge back into a single set of counts/status message.
const runFetch = async () => {
const tvTargets = items.filter(
@@ -985,12 +1070,19 @@ export default function RenamePanel() {
return;
}
// TVmaze needs no token at all, so a TMDB token is only actually required when
// there are movie targets (always TMDB) or TV targets while TMDB is the chosen
// source - a TV-only batch on TVmaze can run with no TMDB token configured at all.
const needsTmdbToken = movieTargets.length > 0 || (tvTargets.length > 0 && tvSource === "tmdb");
if (needsTmdbToken && hasTmdbToken === false) {
setStatus("Add a free TMDB API token in Settings first, then Fetch Data will work.");
// TVmaze needs no credential at all, so TMDB/OMDb are only actually required when
// this batch's TV or movie targets are actually pointed at that source - a TV-only
// batch on TVmaze can run with neither credential configured at all.
const needsTmdbToken =
(movieTargets.length > 0 && movieSource === "tmdb") || (tvTargets.length > 0 && tvSource === "tmdb");
const needsOmdbKey =
(movieTargets.length > 0 && movieSource === "omdb") || (tvTargets.length > 0 && tvSource === "omdb");
if ((needsTmdbToken && hasTmdbToken === false) || (needsOmdbKey && hasOmdbKey === false)) {
const missing = [
needsTmdbToken && hasTmdbToken === false ? "a free TMDB API token" : null,
needsOmdbKey && hasOmdbKey === false ? "a free OMDb API key" : null,
].filter((m): m is string => m !== null);
setStatus(`Add ${missing.join(" and ")} in Settings first, then Fetch Data will work.`);
setSettingsOpen(true);
window.clearTimeout((runFetch as any)._t);
(runFetch as any)._t = window.setTimeout(() => setStatus(null), 4000);
@@ -1363,10 +1455,10 @@ export default function RenamePanel() {
needs a network call, since it's all parsed straight from the filename itself (Aurora Curator 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 online metadata for the real title, episode title, and
air/release date. TV episodes can come from either TMDB or TVmaze — pick which in{" "}
<strong>Fetch Data Settings</strong> (TVmaze is free and needs no token at all); movies always use TMDB
and need a free API token set there 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
air/release date. TV episodes can come from TMDB, TVmaze, or OMDb, and movies from TMDB or OMDb — pick
each independently in <strong>Fetch Data Settings</strong> (TVmaze is free and needs no credential at
all; TMDB and OMDb each need their own free API credential set there first). A batch can
freely mix TV and movie files, even against different sources — 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.
@@ -1486,7 +1578,7 @@ export default function RenamePanel() {
<h2 className="modal-title">Fetch Data Settings</h2>
<span className="modal-field-label">TV episode source</span>
<div className="action-segmented" role="radiogroup" aria-label="Which source Fetch Data uses for TV episodes">
{(["tmdb", "tvmaze"] as const).map((s) => (
{(["tmdb", "tvmaze", "omdb"] as const).map((s) => (
<button
key={s}
type="button"
@@ -1498,17 +1590,38 @@ export default function RenamePanel() {
))}
</div>
<p className="modal-subtitle">{TV_SOURCE_LABELS[tvSource].description}</p>
<span className="modal-field-label">Movie source</span>
<div className="action-segmented" role="radiogroup" aria-label="Which source Fetch Data uses for movies">
{(["tmdb", "omdb"] as const).map((s) => (
<button
key={s}
type="button"
className={`action-segmented-option${movieSource === s ? " selected" : ""}`}
onClick={() => saveMovieSource(s)}
>
{MOVIE_SOURCE_LABELS[s].button}
</button>
))}
</div>
<p className="modal-subtitle">
Movies always resolve against{" "}
{MOVIE_SOURCE_LABELS[movieSource].description} TVmaze isn't an option here — it has no movie catalog.
</p>
<p className="modal-subtitle">
Get a free <strong>API Read Access Token</strong> from your{" "}
<a href="https://www.themoviedb.org/" target="_blank" rel="noreferrer">
TheMovieDB
TMDB
</a>{" "}
regardless of the TV source above — TVmaze has no movie catalog. Get a free{" "}
<strong>API Read Access Token</strong> from your TMDB account under Settings → API, then paste it below.
The token is stored locally on this machine and never leaves it except to call TMDB directly.
account under Settings → API, and/or a free API key from{" "}
<a href="https://www.omdbapi.com/apikey.aspx" target="_blank" rel="noreferrer">
OMDb
</a>
, then paste whichever one(s) you need below. Each credential is stored locally on this machine and
never leaves it except to call that source directly.
</p>
<label htmlFor="tmdb-token" className="modal-field-label">
API Read Access Token
TMDB API Read Access Token
</label>
<input
id="tmdb-token"
@@ -1520,22 +1633,44 @@ export default function RenamePanel() {
onChange={(e) => setTmdbTokenInput(e.target.value)}
placeholder={hasTmdbToken ? "•••••••••• (already saved — enter a new token to replace it)" : "Paste your TMDB token here"}
/>
<div className="modal-actions modal-actions-split">
{hasTmdbToken ? (
<button className="action-button" onClick={clearTmdbToken} disabled={savingSettings}>
Remove Token
</button>
) : (
<span />
)}
<div className="modal-actions">
<button className="action-button" onClick={() => setSettingsOpen(false)} disabled={savingSettings}>
Cancel
</button>
<button className="action-button primary" onClick={saveTmdbToken} disabled={savingSettings}>
{savingSettings ? "Saving…" : "Save"}
</button>
</div>
{hasTmdbToken && (
<button
className="action-button modal-inline-remove"
onClick={clearTmdbToken}
disabled={savingSettings}
>
Remove TMDB Token
</button>
)}
<label htmlFor="omdb-key" className="modal-field-label">
OMDb API Key
</label>
<input
id="omdb-key"
className="modal-text-input"
type="password"
autoComplete="off"
spellCheck={false}
value={omdbKeyInput}
onChange={(e) => setOmdbKeyInput(e.target.value)}
placeholder={hasOmdbKey ? "•••••••••• (already saved — enter a new key to replace it)" : "Paste your OMDb key here"}
/>
{hasOmdbKey && (
<button
className="action-button modal-inline-remove"
onClick={clearOmdbApiKey}
disabled={savingSettings}
>
Remove OMDb Key
</button>
)}
<div className="modal-actions">
<button className="action-button" onClick={() => setSettingsOpen(false)} disabled={savingSettings}>
Cancel
</button>
<button className="action-button primary" onClick={saveCredentials} disabled={savingSettings}>
{savingSettings ? "Saving…" : "Save"}
</button>
</div>
</div>
</div>