use std::path::PathBuf; use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use crate::core::config::OUTPUT_DIR; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TmdbCacheIdentity { pub identity_uuid: String, pub name: String, pub tmdb_id: u64, pub character: String, pub order: u32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TmdbCache { pub file_uuid: String, pub fetched_at: String, pub source: String, pub movie: TmdbMovie, pub cast_count: usize, pub identities_created: usize, #[serde(default)] pub identities: Vec, #[serde(default)] pub cast: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TmdbMovie { pub tmdb_id: u64, pub title: String, pub release_date: Option, pub overview: Option, pub poster_path: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TmdbCastMember { pub name: String, pub character: String, pub profile_path: Option, pub order: u32, pub id: u64, // Person detail fields from /person/{id} pub biography: Option, pub birthday: Option, pub place_of_birth: Option, #[serde(default)] pub also_known_as: Vec, pub imdb_id: Option, pub known_for_department: Option, pub popularity: Option, pub deathday: Option, pub gender: Option, pub homepage: Option, } pub fn tmdb_cache_path(file_uuid: &str) -> PathBuf { PathBuf::from(&*OUTPUT_DIR).join(format!("{}.tmdb.json", file_uuid)) } pub fn read_tmdb_cache(file_uuid: &str) -> Result { let path = tmdb_cache_path(file_uuid); if !path.exists() { anyhow::bail!( "TMDb cache not found: {} (expected: {})", file_uuid, path.display() ); } let content = std::fs::read_to_string(&path) .with_context(|| format!("Failed to read TMDb cache: {}", path.display()))?; serde_json::from_str(&content) .map_err(|e| anyhow::anyhow!("Invalid TMDb cache JSON {}: {}", path.display(), e)) } pub fn write_tmdb_cache(cache: &TmdbCache) -> Result<()> { let path = tmdb_cache_path(&cache.file_uuid); let json = serde_json::to_string_pretty(cache) .with_context(|| format!("Failed to serialize TMDb cache: {}", cache.file_uuid))?; std::fs::write(&path, &json) .with_context(|| format!("Failed to write TMDb cache: {}", path.display()))?; Ok(()) } pub fn delete_tmdb_cache(file_uuid: &str) -> Result<()> { let path = tmdb_cache_path(file_uuid); if path.exists() { std::fs::remove_file(&path) .with_context(|| format!("Failed to delete TMDb cache: {}", path.display()))?; } Ok(()) } pub fn count_cache_files() -> usize { let dir = PathBuf::from(&*OUTPUT_DIR); match std::fs::read_dir(&dir) { Ok(entries) => entries .filter_map(|e| e.ok()) .filter(|e| e.file_name().to_string_lossy().ends_with(".tmdb.json")) .count(), Err(_) => 0, } } #[cfg(test)] pub fn count_cache_files_at(base: &std::path::Path) -> usize { match std::fs::read_dir(base) { Ok(entries) => entries .filter_map(|e| e.ok()) .filter(|e| e.file_name().to_string_lossy().ends_with(".tmdb.json")) .count(), Err(_) => 0, } } #[cfg(test)] pub fn write_tmdb_cache_at(base: &std::path::Path, cache: &TmdbCache) -> Result<()> { std::fs::create_dir_all(base)?; let path = base.join(format!("{}.tmdb.json", cache.file_uuid)); let json = serde_json::to_string_pretty(cache)?; std::fs::write(&path, &json)?; Ok(()) } #[cfg(test)] pub fn read_tmdb_cache_at(base: &std::path::Path, file_uuid: &str) -> Result { let path = base.join(format!("{}.tmdb.json", file_uuid)); if !path.exists() { anyhow::bail!("Cache not found"); } let content = std::fs::read_to_string(&path)?; serde_json::from_str(&content).map_err(Into::into) } #[cfg(test)] mod tests { use super::*; fn sample_cache(file_uuid: &str) -> TmdbCache { TmdbCache { file_uuid: file_uuid.to_string(), fetched_at: "2026-05-16T12:00:00+00:00".to_string(), source: "agent".to_string(), movie: TmdbMovie { tmdb_id: 4808, title: "Charade".to_string(), release_date: Some("1963-12-05".to_string()), overview: Some("A romantic thriller...".to_string()), poster_path: Some("/abc.jpg".to_string()), }, cast: vec![ TmdbCastMember { name: "Cary Grant".to_string(), character: "Peter Joshua".to_string(), profile_path: Some("/cary.jpg".to_string()), order: 0, id: 112, biography: Some("Archibald Alec Leach...".to_string()), birthday: Some("1904-01-18".to_string()), place_of_birth: Some("Bristol, England, UK".to_string()), also_known_as: vec!["Archie Leach".to_string()], imdb_id: Some("nm0000026".to_string()), known_for_department: Some("Acting".to_string()), popularity: Some(28.3), deathday: Some("1986-11-29".to_string()), gender: Some(2), homepage: None, }, TmdbCastMember { name: "Audrey Hepburn".to_string(), character: "Regina Lampert".to_string(), profile_path: Some("/audrey.jpg".to_string()), order: 1, id: 113, biography: Some("Audrey Kathleen Hepburn...".to_string()), birthday: Some("1929-05-04".to_string()), place_of_birth: Some("Ixelles, Belgium".to_string()), also_known_as: vec!["Edda van Heemstra".to_string()], imdb_id: Some("nm0000030".to_string()), known_for_department: Some("Acting".to_string()), popularity: Some(35.7), deathday: Some("1993-01-20".to_string()), gender: Some(1), homepage: None, }, ], cast_count: 20, identities_created: 0, identities: vec![], } } #[test] fn test_cache_path_format() { let p = tmdb_cache_path("abcdef"); assert!(p.to_string_lossy().ends_with("abcdef.tmdb.json")); } #[test] fn test_serde_roundtrip() { let cache = sample_cache("aaaaaaaa"); let json = serde_json::to_string_pretty(&cache).unwrap(); let parsed: TmdbCache = serde_json::from_str(&json).unwrap(); assert_eq!(parsed.file_uuid, "aaaaaaaa"); assert_eq!(parsed.movie.title, "Charade"); assert_eq!(parsed.cast.len(), 2); assert_eq!(parsed.cast[0].name, "Cary Grant"); assert_eq!(parsed.movie.tmdb_id, 4808); } #[test] fn test_write_then_read_cache_at() { let tmp = std::env::temp_dir().join("momentry_test_cache"); let _ = std::fs::remove_dir_all(&tmp); let base = &tmp; let cache = sample_cache("bbbbbbbb"); write_tmdb_cache_at(base, &cache).unwrap(); let read = read_tmdb_cache_at(base, "bbbbbbbb").unwrap(); assert_eq!(read.movie.title, "Charade"); assert_eq!(read.cast[1].id, 113); let _ = std::fs::remove_dir_all(&tmp); } #[test] fn test_read_missing_cache_at_errors() { let tmp = std::env::temp_dir().join("momentry_test_missing"); let _ = std::fs::remove_dir_all(&tmp); let base = &tmp; let result = read_tmdb_cache_at(base, "nonexistent"); assert!(result.is_err()); let _ = std::fs::remove_dir_all(&tmp); } #[test] fn test_count_cache_files_at() { let tmp = std::env::temp_dir().join("momentry_test_count"); let _ = std::fs::remove_dir_all(&tmp); let base = &tmp; assert_eq!(count_cache_files_at(base), 0); let c1 = sample_cache("aaa"); write_tmdb_cache_at(base, &c1).unwrap(); assert_eq!(count_cache_files_at(base), 1); let c2 = sample_cache("bbb"); write_tmdb_cache_at(base, &c2).unwrap(); assert_eq!(count_cache_files_at(base), 2); std::fs::write(base.join("other.json"), "{}").unwrap(); assert_eq!(count_cache_files_at(base), 2); let _ = std::fs::remove_dir_all(&tmp); } }