feat: implement TMDb enrichment for movie profiles

- Add SqliteMovieProfileRepository for managing movie profiles in SQLite.
- Create TmdbEnrichmentClient to fetch movie details from TMDb API.
- Implement enrichment event handling with EnrichmentHandler.
- Introduce periodic jobs for cleaning up expired import sessions and checking for stale movie profiles.
- Update application context to include movie profile repository.
- Add API endpoint to retrieve movie profiles.
- Extend domain models with new structures for movie enrichment (Genre, Keyword, CastMember, CrewMember, MovieProfile).
- Modify event system to include MovieEnrichmentRequested event.
- Enhance tests to cover new functionality and ensure stability.
This commit is contained in:
2026-05-12 13:23:41 +02:00
parent c696a3b780
commit 38d13fbff1
30 changed files with 1193 additions and 30 deletions

View File

@@ -6,8 +6,8 @@ use crate::{
events::{DomainEvent, EventEnvelope},
models::{
AnnotatedRow, DiaryEntry, DiaryFilter, ExportFormat, FeedEntry, FieldMapping,
FileFormat, ImportError, ImportProfile, ImportSession, Movie, MovieStats, ParsedFile,
Review, ReviewHistory, User, UserStats, UserSummary, UserTrends,
FileFormat, ImportError, ImportProfile, ImportSession, Movie, MovieProfile, MovieStats,
ParsedFile, Review, ReviewHistory, User, UserStats, UserSummary, UserTrends,
collections::{PageParams, Paginated},
},
value_objects::{
@@ -217,6 +217,31 @@ pub trait EventHandler: Send + Sync {
async fn handle(&self, event: &DomainEvent) -> Result<(), DomainError>;
}
#[async_trait]
pub trait PeriodicJob: Send + Sync {
fn interval(&self) -> std::time::Duration;
async fn run(&self) -> Result<(), DomainError>;
}
#[async_trait]
pub trait MovieProfileRepository: Send + Sync {
async fn upsert(&self, profile: &MovieProfile) -> Result<(), DomainError>;
async fn get_by_movie_id(&self, id: &MovieId) -> Result<Option<MovieProfile>, DomainError>;
/// Returns (movie_id, external_metadata_id) for movies with no profile or a stale one
/// (enriched_at older than 30 days).
async fn list_stale(&self) -> Result<Vec<(MovieId, String)>, DomainError>;
}
#[async_trait]
pub trait MovieEnrichmentClient: Send + Sync {
/// Resolves an external ID (TMDb or IMDb) and fetches the full movie profile.
async fn fetch_profile(
&self,
movie_id: MovieId,
external_metadata_id: &str,
) -> Result<MovieProfile, DomainError>;
}
#[async_trait]
pub trait ImportSessionRepository: Send + Sync {
async fn create(&self, session: &ImportSession) -> Result<(), DomainError>;