Add TVmaze as a second, user-selectable TV metadata source for Fetch Data

TV episodes can now resolve against either TMDB (default) or TVmaze - picked
via a segmented source picker in the renamed "Fetch Data Settings" modal.
TVmaze is free and needs no API token at all; movies still always resolve
against TMDB regardless of the choice, since TVmaze has no movie catalog.

- New TvmazeClient (crates/aurora-curator-core/src/web/tvmaze.rs): search +
  season/episode lookup against api.tvmaze.com, filtering out unnumbered
  specials rather than panicking on them (TVmaze returns every season's
  episodes in one flat list, unlike TMDB's per-season endpoint).
- New TvMetadataSource setting (settings.rs), persisted alongside the TMDB
  token, defaulting to Tmdb for schema-evolution safety.
- metadata.rs now branches TV fetches through an AnyTvClient wrapper so the
  existing per-(series, season) caching loop doesn't care which source
  produced a result; the movie-fetch path is untouched.
- Frontend: TMDB/TVmaze picker in Fetch Data Settings, persisted on change;
  Fetch Data's token gating now only requires a TMDB token when it's actually
  needed (movies always, TV only when TMDB is the chosen source) instead of
  unconditionally blocking on it.

cargo test --workspace: 88/88 passing (81 + 7 new TVmaze + 3 new settings).
npm run build passing. Playwright verification: 18/18 checks (picker renders
and persists across reload, TV Fetch Data succeeds on TVmaze with zero TMDB
token configured, movies still correctly demand a token even with TVmaze
selected, switching back to TMDB still works end-to-end).
This commit is contained in:
Renamr Scaffold
2026-08-29 10:44:12 +00:00
parent 7097b1ede7
commit 5763c6313b
7 changed files with 729 additions and 96 deletions
+69 -3
View File
@@ -1,6 +1,7 @@
//! Small persisted app settings - currently just the TMDB API token ("Fetch Data" needs
//! it; everything else in the app works offline without any settings at all). Stored as
//! plain JSON in Tauri's per-OS app config directory (e.g.
//! Small persisted app settings - the TMDB API token, and (since the TVmaze datasource
//! was added) which TV metadata source Fetch Data should use ("Fetch Data" is the only
//! thing in the app that needs settings at all; everything else works offline). Stored
//! as plain JSON in Tauri's per-OS app config directory (e.g.
//! `~/Library/Application Support/com.auroracurator.app/settings.json` on macOS,
//! `~/.config/aurora-curator/settings.json` on Linux) - not the original FileBot's config
//! location, since that's Java Preferences-backed and OS/JVM-specific; this is Aurora Curator's
@@ -11,10 +12,28 @@ use std::fs;
use std::path::PathBuf;
use tauri::{AppHandle, Manager};
/// Which online source Fetch Data resolves TV episodes against. Movies always use
/// TMDB regardless of this setting - TVmaze doesn't have a movie catalog at all, so
/// there's nothing to pick between for the movie half of Fetch Data.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum TvMetadataSource {
Tmdb,
Tvmaze,
}
impl Default for TvMetadataSource {
fn default() -> Self {
TvMetadataSource::Tmdb
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
struct StoredSettings {
#[serde(default)]
tmdb_token: Option<String>,
#[serde(default)]
tv_metadata_source: TvMetadataSource,
}
fn settings_path(app: &AppHandle) -> Result<PathBuf, String> {
@@ -59,3 +78,50 @@ pub fn save_tmdb_token(app: AppHandle, token: String) -> Result<(), String> {
settings.tmdb_token = if trimmed.is_empty() { None } else { Some(trimmed) };
save(&app, &settings)
}
/// Returns the stored TV metadata source (defaults to TMDB if nothing's been chosen).
pub fn tv_metadata_source(app: &AppHandle) -> TvMetadataSource {
load(app).tv_metadata_source
}
#[tauri::command]
pub fn get_tv_metadata_source(app: AppHandle) -> TvMetadataSource {
tv_metadata_source(&app)
}
#[tauri::command]
pub fn save_tv_metadata_source(app: AppHandle, source: TvMetadataSource) -> Result<(), String> {
let mut settings = load(&app);
settings.tv_metadata_source = source;
save(&app, &settings)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tv_metadata_source_defaults_to_tmdb() {
assert_eq!(TvMetadataSource::default(), TvMetadataSource::Tmdb);
}
#[test]
fn tv_metadata_source_serializes_as_lowercase_camel_case() {
// The frontend sends/receives this as a plain string ("tmdb"/"tvmaze") - pin
// the exact serde output so a future refactor can't silently change the wire
// format out from under the TS side without a test catching it.
assert_eq!(serde_json::to_string(&TvMetadataSource::Tmdb).unwrap(), "\"tmdb\"");
assert_eq!(serde_json::to_string(&TvMetadataSource::Tvmaze).unwrap(), "\"tvmaze\"");
}
#[test]
fn stored_settings_without_a_tv_metadata_source_field_still_deserializes() {
// Settings written before this field existed (just `{"tmdbToken": "..."}` or
// similar) must still load cleanly, defaulting to Tmdb - same
// schema-evolution pattern used for `HistoryBatch.action` (see project memory).
let old_json = r#"{"tmdb_token":"abc123"}"#;
let parsed: StoredSettings = serde_json::from_str(old_json).unwrap();
assert_eq!(parsed.tv_metadata_source, TvMetadataSource::Tmdb);
assert_eq!(parsed.tmdb_token, Some("abc123".to_string()));
}
}