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:
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user