From 9c06fbd33eb95986d284fe5e585d2c1b29f77a51 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Mon, 29 Jun 2026 23:54:59 +0200 Subject: [PATCH] refactor --- crates/adapters/tmdb-enrichment/Cargo.toml | 1 - crates/adapters/tmdb-enrichment/src/client.rs | 186 +----------------- crates/adapters/tmdb-enrichment/src/lib.rs | 6 +- crates/adapters/tmdb-enrichment/src/movie.rs | 140 +++++++++++++ crates/adapters/tmdb-enrichment/src/person.rs | 47 +++++ crates/application/Cargo.toml | 1 + .../src/movies/event_handler.rs} | 7 +- crates/application/src/movies/mod.rs | 2 + .../src/person/event_handler.rs} | 5 +- crates/application/src/person/mod.rs | 3 + crates/worker/src/main.rs | 4 +- 11 files changed, 207 insertions(+), 195 deletions(-) create mode 100644 crates/adapters/tmdb-enrichment/src/movie.rs create mode 100644 crates/adapters/tmdb-enrichment/src/person.rs rename crates/{adapters/tmdb-enrichment/src/movie_handler.rs => application/src/movies/event_handler.rs} (99%) rename crates/{adapters/tmdb-enrichment/src/person_handler.rs => application/src/person/event_handler.rs} (87%) diff --git a/crates/adapters/tmdb-enrichment/Cargo.toml b/crates/adapters/tmdb-enrichment/Cargo.toml index 548a9ab..b6d28d4 100644 --- a/crates/adapters/tmdb-enrichment/Cargo.toml +++ b/crates/adapters/tmdb-enrichment/Cargo.toml @@ -4,7 +4,6 @@ version = "0.1.0" edition = "2024" [dependencies] -application = { workspace = true } domain = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } diff --git a/crates/adapters/tmdb-enrichment/src/client.rs b/crates/adapters/tmdb-enrichment/src/client.rs index c30a0c8..3fc468e 100644 --- a/crates/adapters/tmdb-enrichment/src/client.rs +++ b/crates/adapters/tmdb-enrichment/src/client.rs @@ -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 { + pub(crate) async fn resolve_tmdb_id(&self, external_id: &str) -> Result { if let Some(numeric) = external_id.strip_prefix("tmdb:") { return numeric.parse::().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 { - 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, - } - #[derive(Deserialize)] - struct CrewDto { - id: u64, - name: String, - job: String, - department: String, - profile_path: Option, - } - #[derive(Deserialize)] - struct Credits { - cast: Vec, - crew: Vec, - } - #[derive(Deserialize)] - struct KeywordDto { - id: u32, - name: String, - } - #[derive(Deserialize)] - struct Keywords { - keywords: Vec, - } - #[derive(Deserialize)] - struct Details { - imdb_id: Option, - overview: Option, - tagline: Option, - runtime: Option, - budget: Option, - revenue: Option, - vote_average: Option, - vote_count: Option, - original_language: Option, - genres: Vec, - belongs_to_collection: Option, - 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 { - let tmdb_id = external_id - .strip_prefix("tmdb:") - .and_then(|s| s.parse::().ok()) - .ok_or_else(|| { - DomainError::InfrastructureError(format!( - "Cannot parse person external_id: {external_id}" - )) - })?; - - #[derive(Deserialize)] - struct PersonDetails { - biography: Option, - birthday: Option, - deathday: Option, - place_of_birth: Option, - also_known_as: Option>, - homepage: Option, - imdb_id: Option, - } - - 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()), - }) - } -} diff --git a/crates/adapters/tmdb-enrichment/src/lib.rs b/crates/adapters/tmdb-enrichment/src/lib.rs index 09bcdab..fa79a43 100644 --- a/crates/adapters/tmdb-enrichment/src/lib.rs +++ b/crates/adapters/tmdb-enrichment/src/lib.rs @@ -1,7 +1,5 @@ mod client; -mod movie_handler; -mod person_handler; +mod movie; +mod person; pub use client::TmdbEnrichmentClient; -pub use movie_handler::MovieEnrichmentHandler; -pub use person_handler::PersonEnrichmentHandler; diff --git a/crates/adapters/tmdb-enrichment/src/movie.rs b/crates/adapters/tmdb-enrichment/src/movie.rs new file mode 100644 index 0000000..dd5e371 --- /dev/null +++ b/crates/adapters/tmdb-enrichment/src/movie.rs @@ -0,0 +1,140 @@ +use async_trait::async_trait; +use chrono::Utc; +use domain::{ + errors::DomainError, + models::{CastMember, CrewMember, Genre, Keyword, MovieProfile}, + ports::MovieEnrichmentClient, + value_objects::MovieId, +}; +use serde::Deserialize; + +use crate::client::TmdbEnrichmentClient; + +#[async_trait] +impl MovieEnrichmentClient for TmdbEnrichmentClient { + async fn fetch_profile( + &self, + movie_id: MovieId, + external_metadata_id: &str, + ) -> Result { + 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, + } + #[derive(Deserialize)] + struct CrewDto { + id: u64, + name: String, + job: String, + department: String, + profile_path: Option, + } + #[derive(Deserialize)] + struct Credits { + cast: Vec, + crew: Vec, + } + #[derive(Deserialize)] + struct KeywordDto { + id: u32, + name: String, + } + #[derive(Deserialize)] + struct Keywords { + keywords: Vec, + } + #[derive(Deserialize)] + struct Details { + imdb_id: Option, + overview: Option, + tagline: Option, + runtime: Option, + budget: Option, + revenue: Option, + vote_average: Option, + vote_count: Option, + original_language: Option, + genres: Vec, + belongs_to_collection: Option, + 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(), + }) + } +} diff --git a/crates/adapters/tmdb-enrichment/src/person.rs b/crates/adapters/tmdb-enrichment/src/person.rs new file mode 100644 index 0000000..040b933 --- /dev/null +++ b/crates/adapters/tmdb-enrichment/src/person.rs @@ -0,0 +1,47 @@ +use async_trait::async_trait; +use domain::{errors::DomainError, models::PersonEnrichmentData, ports::PersonEnrichmentClient}; +use serde::Deserialize; + +use crate::client::TmdbEnrichmentClient; + +#[async_trait] +impl PersonEnrichmentClient for TmdbEnrichmentClient { + async fn fetch_details(&self, external_id: &str) -> Result { + let tmdb_id = external_id + .strip_prefix("tmdb:") + .and_then(|s| s.parse::().ok()) + .ok_or_else(|| { + DomainError::InfrastructureError(format!( + "Cannot parse person external_id: {external_id}" + )) + })?; + + #[derive(Deserialize)] + struct PersonDetails { + biography: Option, + birthday: Option, + deathday: Option, + place_of_birth: Option, + also_known_as: Option>, + homepage: Option, + imdb_id: Option, + } + + 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()), + }) + } +} diff --git a/crates/application/Cargo.toml b/crates/application/Cargo.toml index cb47320..97bfe1c 100644 --- a/crates/application/Cargo.toml +++ b/crates/application/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] async-trait = { workspace = true } domain = { workspace = true } +reqwest = { workspace = true } uuid = { workspace = true } chrono = { workspace = true } tracing = { workspace = true } diff --git a/crates/adapters/tmdb-enrichment/src/movie_handler.rs b/crates/application/src/movies/event_handler.rs similarity index 99% rename from crates/adapters/tmdb-enrichment/src/movie_handler.rs rename to crates/application/src/movies/event_handler.rs index 89f5d86..50b83b5 100644 --- a/crates/adapters/tmdb-enrichment/src/movie_handler.rs +++ b/crates/application/src/movies/event_handler.rs @@ -1,8 +1,5 @@ use std::sync::Arc; -use application::movies::{ - commands::EnrichMovieCommand, deps::EnrichMovieDeps, enrich_movie, request_enrichment, -}; use async_trait::async_trait; use domain::{ errors::DomainError, @@ -14,6 +11,10 @@ use domain::{ }, }; +use crate::movies::{ + commands::EnrichMovieCommand, deps::EnrichMovieDeps, enrich_movie, request_enrichment, +}; + pub struct MovieEnrichmentHandler { enrichment_client: Arc, movie_repository: Arc, diff --git a/crates/application/src/movies/mod.rs b/crates/application/src/movies/mod.rs index b82cdf7..ceeff85 100644 --- a/crates/application/src/movies/mod.rs +++ b/crates/application/src/movies/mod.rs @@ -2,6 +2,7 @@ pub mod commands; pub mod deps; pub mod discovery_indexer; pub mod enrich_movie; +pub mod event_handler; pub mod get_movie_profile; pub mod get_movies; pub mod queries; @@ -11,5 +12,6 @@ pub mod search_cleanup; pub mod sync_poster; pub use discovery_indexer::MovieDiscoveryIndexer; +pub use event_handler::MovieEnrichmentHandler; pub use reindex_search::SearchReindexHandler; pub use search_cleanup::SearchCleanupHandler; diff --git a/crates/adapters/tmdb-enrichment/src/person_handler.rs b/crates/application/src/person/event_handler.rs similarity index 87% rename from crates/adapters/tmdb-enrichment/src/person_handler.rs rename to crates/application/src/person/event_handler.rs index 72aa4da..9d99b93 100644 --- a/crates/adapters/tmdb-enrichment/src/person_handler.rs +++ b/crates/application/src/person/event_handler.rs @@ -7,7 +7,7 @@ use domain::{ ports::{EventHandler, PersonCommand, PersonEnrichmentClient, PersonQuery}, }; -use application::person::deps::EnrichPersonDeps; +use super::deps::EnrichPersonDeps; pub struct PersonEnrichmentHandler { deps: EnrichPersonDeps, @@ -40,7 +40,6 @@ impl EventHandler for PersonEnrichmentHandler { _ => return Ok(()), }; - application::person::enrich::execute(&self.deps, person_id, external_person_id.value()) - .await + super::enrich::execute(&self.deps, person_id, external_person_id.value()).await } } diff --git a/crates/application/src/person/mod.rs b/crates/application/src/person/mod.rs index 40552e0..e507ee2 100644 --- a/crates/application/src/person/mod.rs +++ b/crates/application/src/person/mod.rs @@ -1,4 +1,7 @@ pub mod deps; pub mod enrich; +pub mod event_handler; pub mod get; pub mod get_credits; + +pub use event_handler::PersonEnrichmentHandler; diff --git a/crates/worker/src/main.rs b/crates/worker/src/main.rs index c8e8579..1720a80 100644 --- a/crates/worker/src/main.rs +++ b/crates/worker/src/main.rs @@ -92,7 +92,7 @@ async fn main() -> anyhow::Result<()> { Ok(client) => { tracing::info!("TMDb enrichment enabled"); let client = Arc::new(client); - let handler = Arc::new(tmdb_enrichment::MovieEnrichmentHandler::new( + let handler = Arc::new(application::movies::MovieEnrichmentHandler::new( Arc::clone(&client) as Arc, Arc::clone(&movie), Arc::clone(&movie_profile), @@ -101,7 +101,7 @@ async fn main() -> anyhow::Result<()> { Arc::clone(&object_storage), )) as Arc; let person_enrichment_arc = Arc::clone(&client) as Arc; - let person_handler = Arc::new(tmdb_enrichment::PersonEnrichmentHandler::new( + let person_handler = Arc::new(application::person::PersonEnrichmentHandler::new( Arc::clone(&person_query), Some(person_enrichment_arc), Arc::clone(&person_command),