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:
+102
-13
@@ -1,6 +1,6 @@
|
||||
//! 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
|
||||
//! Small persisted app settings - the TMDB API token, the OMDb API key, and which TV
|
||||
//! and movie metadata sources 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 the OS's standard per-app config directory, joined with the app's
|
||||
//! bundle identifier - `~/Library/Application Support/com.auroracurator.app/settings.json`
|
||||
//! on macOS, `~/.config/com.auroracurator.app/settings.json` on Linux, `%APPDATA%\
|
||||
@@ -11,10 +11,10 @@
|
||||
//! The GUI reaches that directory through a running Tauri `AppHandle`
|
||||
//! (`app.path().app_config_dir()`); the `-rename` CLI mode (`cli.rs`) never builds a
|
||||
//! Tauri app at all, so it needs the exact same directory computed a different way -
|
||||
//! see [`cli_tmdb_token`] below, which replicates Tauri's own `dirs::config_dir()
|
||||
//! .join(identifier)` computation directly (confirmed against Tauri 2.11's
|
||||
//! `path/desktop.rs` source) so both code paths are guaranteed to read/write the same
|
||||
//! file. [`APP_CONFIG_IDENTIFIER`] must be kept in sync with `tauri.conf.json`'s
|
||||
//! see [`cli_tmdb_token`]/[`cli_omdb_api_key`] below, which replicate Tauri's own
|
||||
//! `dirs::config_dir().join(identifier)` computation directly (confirmed against Tauri
|
||||
//! 2.11's `path/desktop.rs` source) so both code paths are guaranteed to read/write the
|
||||
//! same file. [`APP_CONFIG_IDENTIFIER`] must be kept in sync with `tauri.conf.json`'s
|
||||
//! `identifier` field by hand - there's no way to share a constant with the JSON config.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -25,14 +25,15 @@ use tauri::{AppHandle, Manager};
|
||||
/// Must match `tauri.conf.json`'s `identifier` field exactly - see the module doc.
|
||||
const APP_CONFIG_IDENTIFIER: &str = "com.auroracurator.app";
|
||||
|
||||
/// 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.
|
||||
/// Which online source Fetch Data resolves TV episodes against. TVmaze needs no API
|
||||
/// key at all; TMDB and OMDb each need their own saved credential (see
|
||||
/// [`tmdb_token`]/[`omdb_api_key`]) before a batch resolved against them will succeed.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum TvMetadataSource {
|
||||
Tmdb,
|
||||
Tvmaze,
|
||||
Omdb,
|
||||
}
|
||||
|
||||
impl Default for TvMetadataSource {
|
||||
@@ -41,12 +42,32 @@ impl Default for TvMetadataSource {
|
||||
}
|
||||
}
|
||||
|
||||
/// Which online source Fetch Data resolves movies against. Unlike TV, there's no
|
||||
/// zero-setup option here (TVmaze has no movie catalog) - both choices need their own
|
||||
/// saved credential.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum MovieMetadataSource {
|
||||
Tmdb,
|
||||
Omdb,
|
||||
}
|
||||
|
||||
impl Default for MovieMetadataSource {
|
||||
fn default() -> Self {
|
||||
MovieMetadataSource::Tmdb
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
struct StoredSettings {
|
||||
#[serde(default)]
|
||||
tmdb_token: Option<String>,
|
||||
#[serde(default)]
|
||||
omdb_api_key: Option<String>,
|
||||
#[serde(default)]
|
||||
tv_metadata_source: TvMetadataSource,
|
||||
#[serde(default)]
|
||||
movie_metadata_source: MovieMetadataSource,
|
||||
}
|
||||
|
||||
fn settings_path(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
@@ -106,6 +127,36 @@ pub fn save_tmdb_token(app: AppHandle, token: String) -> Result<(), String> {
|
||||
save(&app, &settings)
|
||||
}
|
||||
|
||||
/// Returns the stored OMDb API key, or `None` if nothing (or only blank text) is
|
||||
/// saved. Mirrors [`tmdb_token`] exactly - OMDb calls its credential a "key" rather
|
||||
/// than a "token", but it's the same kind of thing (a plain saved secret, entered once
|
||||
/// through Fetch Data Settings, never handled by anything but the user and this app).
|
||||
pub fn omdb_api_key(app: &AppHandle) -> Option<String> {
|
||||
load(app).omdb_api_key.filter(|t| !t.trim().is_empty())
|
||||
}
|
||||
|
||||
/// Same as [`omdb_api_key`], but for the `-rename` CLI path - mirrors [`cli_tmdb_token`]
|
||||
/// exactly, see its doc comment for why this can't just ask an `AppHandle`.
|
||||
pub fn cli_omdb_api_key() -> Option<String> {
|
||||
let dir = dirs::config_dir()?.join(APP_CONFIG_IDENTIFIER);
|
||||
let contents = fs::read_to_string(dir.join("settings.json")).ok()?;
|
||||
let settings: StoredSettings = serde_json::from_str(&contents).ok()?;
|
||||
settings.omdb_api_key.filter(|t| !t.trim().is_empty())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_omdb_api_key(app: AppHandle) -> Option<String> {
|
||||
omdb_api_key(&app)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn save_omdb_api_key(app: AppHandle, key: String) -> Result<(), String> {
|
||||
let mut settings = load(&app);
|
||||
let trimmed = key.trim().to_string();
|
||||
settings.omdb_api_key = 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
|
||||
@@ -123,6 +174,24 @@ pub fn save_tv_metadata_source(app: AppHandle, source: TvMetadataSource) -> Resu
|
||||
save(&app, &settings)
|
||||
}
|
||||
|
||||
/// Returns the stored movie metadata source (defaults to TMDB if nothing's been
|
||||
/// chosen - movies were TMDB-only before OMDb added a second option).
|
||||
pub fn movie_metadata_source(app: &AppHandle) -> MovieMetadataSource {
|
||||
load(app).movie_metadata_source
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_movie_metadata_source(app: AppHandle) -> MovieMetadataSource {
|
||||
movie_metadata_source(&app)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn save_movie_metadata_source(app: AppHandle, source: MovieMetadataSource) -> Result<(), String> {
|
||||
let mut settings = load(&app);
|
||||
settings.movie_metadata_source = source;
|
||||
save(&app, &settings)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -134,11 +203,19 @@ mod tests {
|
||||
|
||||
#[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.
|
||||
// The frontend sends/receives this as a plain string ("tmdb"/"tvmaze"/"omdb") -
|
||||
// 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\"");
|
||||
assert_eq!(serde_json::to_string(&TvMetadataSource::Omdb).unwrap(), "\"omdb\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn movie_metadata_source_defaults_to_tmdb_and_serializes_as_lowercase() {
|
||||
assert_eq!(MovieMetadataSource::default(), MovieMetadataSource::Tmdb);
|
||||
assert_eq!(serde_json::to_string(&MovieMetadataSource::Tmdb).unwrap(), "\"tmdb\"");
|
||||
assert_eq!(serde_json::to_string(&MovieMetadataSource::Omdb).unwrap(), "\"omdb\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -169,4 +246,16 @@ mod tests {
|
||||
assert_eq!(parsed.tv_metadata_source, TvMetadataSource::Tmdb);
|
||||
assert_eq!(parsed.tmdb_token, Some("abc123".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stored_settings_without_omdb_fields_still_deserializes() {
|
||||
// Same schema-evolution guarantee as the TV-metadata-source test above, for the
|
||||
// two fields OMDb support added: an old settings.json with neither
|
||||
// `omdb_api_key` nor `movie_metadata_source` present must still load cleanly.
|
||||
let old_json = r#"{"tmdb_token":"abc123","tv_metadata_source":"tvmaze"}"#;
|
||||
let parsed: StoredSettings = serde_json::from_str(old_json).unwrap();
|
||||
assert_eq!(parsed.omdb_api_key, None);
|
||||
assert_eq!(parsed.movie_metadata_source, MovieMetadataSource::Tmdb);
|
||||
assert_eq!(parsed.tv_metadata_source, TvMetadataSource::Tvmaze);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user