Files
momentry_core/src/core/tmdb/cache.rs

265 lines
8.6 KiB
Rust

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<TmdbCacheIdentity>,
#[serde(default)]
pub cast: Vec<TmdbCastMember>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TmdbMovie {
pub tmdb_id: u64,
pub title: String,
pub release_date: Option<String>,
pub overview: Option<String>,
pub poster_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TmdbCastMember {
pub name: String,
pub character: String,
pub profile_path: Option<String>,
pub order: u32,
pub id: u64,
// Person detail fields from /person/{id}
pub biography: Option<String>,
pub birthday: Option<String>,
pub place_of_birth: Option<String>,
#[serde(default)]
pub also_known_as: Vec<String>,
pub imdb_id: Option<String>,
pub known_for_department: Option<String>,
pub popularity: Option<f64>,
pub deathday: Option<String>,
pub gender: Option<i32>,
pub homepage: Option<String>,
}
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<TmdbCache> {
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<TmdbCache> {
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);
}
}