2026-08-29 10:44:12 +00:00
|
|
|
//! 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
|
2026-08-29 11:30:51 +00:00
|
|
|
//! 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%\
|
|
|
|
|
//! com.auroracurator.app\settings.json` on Windows - not the original FileBot's config
|
|
|
|
|
//! location, since that's Java Preferences-backed and OS/JVM-specific; this is Aurora
|
|
|
|
|
//! Curator's own, analogous location for its own settings.
|
|
|
|
|
//!
|
|
|
|
|
//! 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
|
|
|
|
|
//! `identifier` field by hand - there's no way to share a constant with the JSON config.
|
2026-08-29 03:01:19 +00:00
|
|
|
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use std::fs;
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
use tauri::{AppHandle, Manager};
|
|
|
|
|
|
2026-08-29 11:30:51 +00:00
|
|
|
/// Must match `tauri.conf.json`'s `identifier` field exactly - see the module doc.
|
|
|
|
|
const APP_CONFIG_IDENTIFIER: &str = "com.auroracurator.app";
|
|
|
|
|
|
2026-08-29 10:44:12 +00:00
|
|
|
/// 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-29 03:01:19 +00:00
|
|
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
|
|
|
|
struct StoredSettings {
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
tmdb_token: Option<String>,
|
2026-08-29 10:44:12 +00:00
|
|
|
#[serde(default)]
|
|
|
|
|
tv_metadata_source: TvMetadataSource,
|
2026-08-29 03:01:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn settings_path(app: &AppHandle) -> Result<PathBuf, String> {
|
|
|
|
|
let dir = app
|
|
|
|
|
.path()
|
|
|
|
|
.app_config_dir()
|
|
|
|
|
.map_err(|e| format!("couldn't resolve the app config directory: {e}"))?;
|
|
|
|
|
fs::create_dir_all(&dir).map_err(|e| format!("couldn't create {}: {e}", dir.display()))?;
|
|
|
|
|
Ok(dir.join("settings.json"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn load(app: &AppHandle) -> StoredSettings {
|
|
|
|
|
let Ok(path) = settings_path(app) else {
|
|
|
|
|
return StoredSettings::default();
|
|
|
|
|
};
|
|
|
|
|
fs::read_to_string(path)
|
|
|
|
|
.ok()
|
|
|
|
|
.and_then(|contents| serde_json::from_str(&contents).ok())
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn save(app: &AppHandle, settings: &StoredSettings) -> Result<(), String> {
|
|
|
|
|
let path = settings_path(app)?;
|
|
|
|
|
let json = serde_json::to_string_pretty(settings).map_err(|e| e.to_string())?;
|
|
|
|
|
fs::write(&path, json).map_err(|e| format!("couldn't write {}: {e}", path.display()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns the stored TMDB token, or `None` if nothing (or only blank text) is saved.
|
|
|
|
|
pub fn tmdb_token(app: &AppHandle) -> Option<String> {
|
|
|
|
|
load(app).tmdb_token.filter(|t| !t.trim().is_empty())
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-29 11:30:51 +00:00
|
|
|
/// Same as [`tmdb_token`], but for the `-rename` CLI path, which has no `AppHandle` to
|
|
|
|
|
/// ask (it never builds a Tauri app at all - see `cli.rs`'s module doc). Resolves the
|
|
|
|
|
/// config directory itself using the identical `dirs::config_dir().join(identifier)`
|
|
|
|
|
/// computation Tauri's own `AppHandle::path().app_config_dir()` uses internally, so a
|
|
|
|
|
/// token saved through the GUI's Fetch Data Settings is found here too. Aurora Curator
|
|
|
|
|
/// still never accepts an API token as a CLI argument - `--db tmdb` only works once a
|
|
|
|
|
/// token has been saved through the app itself at least once.
|
|
|
|
|
pub fn cli_tmdb_token() -> 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.tmdb_token.filter(|t| !t.trim().is_empty())
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-29 03:01:19 +00:00
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn get_tmdb_token(app: AppHandle) -> Option<String> {
|
|
|
|
|
tmdb_token(&app)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tauri::command]
|
|
|
|
|
pub fn save_tmdb_token(app: AppHandle, token: String) -> Result<(), String> {
|
|
|
|
|
let mut settings = load(&app);
|
|
|
|
|
let trimmed = token.trim().to_string();
|
|
|
|
|
settings.tmdb_token = if trimmed.is_empty() { None } else { Some(trimmed) };
|
|
|
|
|
save(&app, &settings)
|
|
|
|
|
}
|
2026-08-29 10:44:12 +00:00
|
|
|
|
|
|
|
|
/// 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\"");
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-29 11:30:51 +00:00
|
|
|
#[test]
|
|
|
|
|
fn app_config_identifier_matches_tauri_conf_json() {
|
|
|
|
|
// `cli_tmdb_token()` recomputes the GUI's settings-file path by hand (no
|
|
|
|
|
// `AppHandle` available in `-rename` mode), so this constant has to be kept in
|
|
|
|
|
// sync with `tauri.conf.json`'s `identifier` field manually - see the module
|
|
|
|
|
// doc. There's no way to share a single source of truth between a Rust const
|
|
|
|
|
// and the JSON config, so this test at least pins the value we're relying on:
|
|
|
|
|
// a change to `tauri.conf.json` without a matching change here would silently
|
|
|
|
|
// make the CLI look in the wrong directory for a token the GUI already saved.
|
|
|
|
|
let conf = include_str!("../tauri.conf.json");
|
|
|
|
|
let parsed: serde_json::Value = serde_json::from_str(conf).unwrap();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
parsed["identifier"].as_str(),
|
|
|
|
|
Some(APP_CONFIG_IDENTIFIER),
|
|
|
|
|
"APP_CONFIG_IDENTIFIER must match tauri.conf.json's identifier field"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-29 10:44:12 +00:00
|
|
|
#[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()));
|
|
|
|
|
}
|
|
|
|
|
}
|