This commit is contained in:
2026-06-29 23:54:59 +02:00
parent bb17beb80d
commit 9c06fbd33e
11 changed files with 207 additions and 195 deletions

View File

@@ -1,16 +1,9 @@
use async_trait::async_trait;
use chrono::Utc;
use domain::{
errors::DomainError,
models::{CastMember, CrewMember, Genre, Keyword, MovieProfile, PersonEnrichmentData},
ports::{MovieEnrichmentClient, PersonEnrichmentClient},
value_objects::MovieId,
};
use domain::errors::DomainError;
use serde::Deserialize;
pub struct TmdbEnrichmentClient {
api_key: String,
http: reqwest::Client,
pub(crate) api_key: String,
pub(crate) http: reqwest::Client,
}
impl TmdbEnrichmentClient {
@@ -49,7 +42,7 @@ impl TmdbEnrichmentClient {
.map_err(|e| DomainError::InfrastructureError(e.to_string()))
}
async fn resolve_tmdb_id(&self, external_id: &str) -> Result<u64, DomainError> {
pub(crate) async fn resolve_tmdb_id(&self, external_id: &str) -> Result<u64, DomainError> {
if let Some(numeric) = external_id.strip_prefix("tmdb:") {
return numeric.parse::<u64>().map_err(|_| {
DomainError::InfrastructureError(format!("Invalid tmdb id: {numeric}"))
@@ -74,174 +67,3 @@ impl TmdbEnrichmentClient {
.ok_or_else(|| DomainError::NotFound(format!("TMDb: no movie for {external_id}")))
}
}
#[async_trait]
impl MovieEnrichmentClient for TmdbEnrichmentClient {
async fn fetch_profile(
&self,
movie_id: MovieId,
external_metadata_id: &str,
) -> Result<MovieProfile, DomainError> {
let tmdb_id = self.resolve_tmdb_id(external_metadata_id).await?;
#[derive(Deserialize)]
struct GenreDto {
id: u32,
name: String,
}
#[derive(Deserialize)]
struct CollectionDto {
name: String,
}
#[derive(Deserialize)]
struct CastDto {
id: u64,
name: String,
character: String,
order: u32,
profile_path: Option<String>,
}
#[derive(Deserialize)]
struct CrewDto {
id: u64,
name: String,
job: String,
department: String,
profile_path: Option<String>,
}
#[derive(Deserialize)]
struct Credits {
cast: Vec<CastDto>,
crew: Vec<CrewDto>,
}
#[derive(Deserialize)]
struct KeywordDto {
id: u32,
name: String,
}
#[derive(Deserialize)]
struct Keywords {
keywords: Vec<KeywordDto>,
}
#[derive(Deserialize)]
struct Details {
imdb_id: Option<String>,
overview: Option<String>,
tagline: Option<String>,
runtime: Option<u32>,
budget: Option<i64>,
revenue: Option<i64>,
vote_average: Option<f64>,
vote_count: Option<u32>,
original_language: Option<String>,
genres: Vec<GenreDto>,
belongs_to_collection: Option<CollectionDto>,
credits: Credits,
keywords: Keywords,
}
let url = self.base(&format!("/movie/{}", tmdb_id));
let d: Details = self
.get(&url, &[("append_to_response", "credits,keywords")])
.await?;
Ok(MovieProfile {
movie_id,
tmdb_id,
imdb_id: d.imdb_id.filter(|s| !s.is_empty()),
overview: d.overview.filter(|s| !s.is_empty()),
tagline: d.tagline.filter(|s| !s.is_empty()),
runtime_minutes: d.runtime,
budget_usd: d.budget.filter(|&v| v > 0),
revenue_usd: d.revenue.filter(|&v| v > 0),
vote_average: d.vote_average,
vote_count: d.vote_count,
original_language: d.original_language,
collection_name: d.belongs_to_collection.map(|c| c.name),
genres: d
.genres
.into_iter()
.map(|g| Genre {
tmdb_id: g.id,
name: g.name,
})
.collect(),
keywords: d
.keywords
.keywords
.into_iter()
.map(|k| Keyword {
tmdb_id: k.id,
name: k.name,
})
.collect(),
cast: d
.credits
.cast
.into_iter()
.map(|c| CastMember {
tmdb_person_id: c.id,
name: c.name,
character: c.character,
billing_order: c.order,
profile_path: c.profile_path,
})
.collect(),
crew: d
.credits
.crew
.into_iter()
.map(|c| CrewMember {
tmdb_person_id: c.id,
name: c.name,
job: c.job,
department: c.department,
profile_path: c.profile_path,
})
.collect(),
enriched_at: Utc::now(),
})
}
}
#[async_trait]
impl PersonEnrichmentClient for TmdbEnrichmentClient {
async fn fetch_details(&self, external_id: &str) -> Result<PersonEnrichmentData, DomainError> {
let tmdb_id = external_id
.strip_prefix("tmdb:")
.and_then(|s| s.parse::<u64>().ok())
.ok_or_else(|| {
DomainError::InfrastructureError(format!(
"Cannot parse person external_id: {external_id}"
))
})?;
#[derive(Deserialize)]
struct PersonDetails {
biography: Option<String>,
birthday: Option<String>,
deathday: Option<String>,
place_of_birth: Option<String>,
also_known_as: Option<Vec<String>>,
homepage: Option<String>,
imdb_id: Option<String>,
}
let url = self.base(&format!("/person/{tmdb_id}"));
let d: PersonDetails = self.get(&url, &[]).await?;
Ok(PersonEnrichmentData {
biography: d.biography.filter(|s| !s.is_empty()),
birthday: d
.birthday
.and_then(|s| chrono::NaiveDate::parse_from_str(&s, "%Y-%m-%d").ok()),
deathday: d
.deathday
.and_then(|s| chrono::NaiveDate::parse_from_str(&s, "%Y-%m-%d").ok()),
place_of_birth: d.place_of_birth.filter(|s| !s.is_empty()),
also_known_as: d.also_known_as.unwrap_or_default(),
homepage: d.homepage.filter(|s| !s.is_empty()),
imdb_id: d.imdb_id.filter(|s| !s.is_empty()),
})
}
}