feat: MovieDto enrichment, movie detail page, PWA, watchlist, watchlist federation

This commit is contained in:
2026-05-13 00:23:45 +02:00
parent 2fd8734d23
commit 53df90ab1f
84 changed files with 2755 additions and 398 deletions

View File

@@ -1,10 +1,10 @@
use chrono::NaiveDateTime;
use domain::{
errors::DomainError,
models::{DiaryEntry, FeedEntry, Movie, Review, ReviewSource, UserSummary},
models::{DiaryEntry, FeedEntry, Movie, MovieSummary, Review, ReviewSource, UserSummary, WatchlistEntry, WatchlistWithMovie},
value_objects::{
Comment, Email, ExternalMetadataId, MovieId, MovieTitle, PosterPath, Rating, ReleaseYear,
ReviewId, UserId,
ReviewId, UserId, WatchlistEntryId,
},
};
use uuid::Uuid;
@@ -20,7 +20,7 @@ pub(crate) struct MovieRow {
}
impl MovieRow {
pub fn to_domain(self) -> Result<Movie, DomainError> {
pub fn into_domain(self) -> Result<Movie, DomainError> {
let id = MovieId::from_uuid(parse_uuid(&self.id)?);
let external_metadata_id = self
.external_metadata_id
@@ -40,6 +40,47 @@ impl MovieRow {
}
}
#[derive(sqlx::FromRow)]
pub(crate) struct MovieSummaryRow {
pub id: String,
pub external_metadata_id: Option<String>,
pub title: String,
pub release_year: i64,
pub director: Option<String>,
pub poster_path: Option<String>,
pub genres: Option<String>,
pub runtime_minutes: Option<i64>,
pub original_language: Option<String>,
pub overview: Option<String>,
pub collection_name: Option<String>,
}
impl MovieSummaryRow {
pub fn into_domain(self) -> Result<MovieSummary, DomainError> {
let movie = MovieRow {
id: self.id,
external_metadata_id: self.external_metadata_id,
title: self.title,
release_year: self.release_year,
director: self.director,
poster_path: self.poster_path,
}
.into_domain()?;
let genres = self
.genres
.map(|g| g.split(',').map(str::to_string).collect())
.unwrap_or_default();
Ok(MovieSummary {
movie,
genres,
runtime_minutes: self.runtime_minutes.map(|v| v as u32),
original_language: self.original_language,
overview: self.overview,
collection_name: self.collection_name,
})
}
}
#[derive(sqlx::FromRow)]
pub(crate) struct ReviewRow {
pub id: String,
@@ -53,7 +94,7 @@ pub(crate) struct ReviewRow {
}
impl ReviewRow {
pub fn to_domain(self) -> Result<Review, DomainError> {
pub fn into_domain(self) -> Result<Review, DomainError> {
let id = ReviewId::from_uuid(parse_uuid(&self.id)?);
let movie_id = MovieId::from_uuid(parse_uuid(&self.movie_id)?);
let user_id = UserId::from_uuid(parse_uuid(&self.user_id)?);
@@ -91,7 +132,7 @@ pub(crate) struct DiaryRow {
}
impl DiaryRow {
pub fn to_domain(self) -> Result<DiaryEntry, DomainError> {
pub fn into_domain(self) -> Result<DiaryEntry, DomainError> {
let movie = MovieRow {
id: self.id,
external_metadata_id: self.external_metadata_id,
@@ -100,7 +141,7 @@ impl DiaryRow {
director: self.director,
poster_path: self.poster_path,
}
.to_domain()?;
.into_domain()?;
let review = ReviewRow {
id: self.review_id,
@@ -112,7 +153,7 @@ impl DiaryRow {
created_at: self.created_at,
remote_actor_url: self.remote_actor_url,
}
.to_domain()?;
.into_domain()?;
Ok(DiaryEntry::new(movie, review))
}
@@ -131,7 +172,7 @@ pub(crate) struct MovieStatsRow {
}
impl MovieStatsRow {
pub fn to_domain(self) -> domain::models::MovieStats {
pub fn into_domain(self) -> domain::models::MovieStats {
domain::models::MovieStats {
total_count: self.total_count as u64,
avg_rating: self.avg_rating,
@@ -168,7 +209,7 @@ pub(crate) struct FeedRow {
}
impl FeedRow {
pub fn to_domain(self) -> Result<FeedEntry, DomainError> {
pub fn into_domain(self) -> Result<FeedEntry, DomainError> {
let diary = DiaryRow {
id: self.id,
external_metadata_id: self.external_metadata_id,
@@ -185,7 +226,7 @@ impl FeedRow {
created_at: self.created_at,
remote_actor_url: self.remote_actor_url,
}
.to_domain()?;
.into_domain()?;
Ok(FeedEntry::new(diary, self.user_email))
}
}
@@ -199,7 +240,7 @@ pub(crate) struct UserSummaryRow {
}
impl UserSummaryRow {
pub fn to_domain(self) -> Result<UserSummary, DomainError> {
pub fn into_domain(self) -> Result<UserSummary, DomainError> {
Ok(UserSummary::new(
UserId::from_uuid(parse_uuid(&self.id)?),
Email::new(self.email)?,
@@ -228,6 +269,41 @@ pub(crate) struct MonthlyRatingRow {
pub count: i64,
}
#[derive(sqlx::FromRow)]
pub(crate) struct WatchlistRow {
pub id: String,
pub user_id: String,
pub movie_id: String,
pub added_at: String,
pub m_id: String,
pub external_metadata_id: Option<String>,
pub title: String,
pub release_year: i64,
pub director: Option<String>,
pub poster_path: Option<String>,
}
impl WatchlistRow {
pub fn into_domain(self) -> Result<WatchlistWithMovie, DomainError> {
let entry = WatchlistEntry {
id: WatchlistEntryId::from_uuid(parse_uuid(&self.id)?),
user_id: UserId::from_uuid(parse_uuid(&self.user_id)?),
movie_id: MovieId::from_uuid(parse_uuid(&self.movie_id)?),
added_at: parse_datetime(&self.added_at)?,
};
let movie = MovieRow {
id: self.m_id,
external_metadata_id: self.external_metadata_id,
title: self.title,
release_year: self.release_year,
director: self.director,
poster_path: self.poster_path,
}
.into_domain()?;
Ok(WatchlistWithMovie { entry, movie })
}
}
pub(crate) fn parse_uuid(s: &str) -> Result<Uuid, DomainError> {
Uuid::parse_str(s)
.map_err(|e| DomainError::InfrastructureError(format!("Invalid UUID '{}': {}", s, e)))