feat: favorite actors top-5 stat in profile trends
All checks were successful
CI / Check / Test (push) Successful in 1h5m14s

This commit is contained in:
2026-08-09 20:59:18 +02:00
parent d282e8ea7e
commit 4d221e60de
13 changed files with 297 additions and 10 deletions

View File

@@ -275,3 +275,12 @@ pub(crate) struct WatchMediumCountRow {
pub watch_medium: String,
pub count: i64,
}
#[derive(sqlx::FromRow)]
pub(crate) struct ActorAppearanceRow {
pub tmdb_person_id: i64,
pub name: String,
pub profile_path: Option<String>,
pub billing_order: i32,
pub movie_id: String,
}

View File

@@ -1,15 +1,17 @@
use async_trait::async_trait;
use domain::{
errors::DomainError,
models::{DirectorStat, MonthlyRating, UserStats, UserTrends},
models::{
ActorAppearance, DirectorStat, MonthlyRating, UserStats, UserTrends, compute_top_actors,
},
ports::StatsRepository,
value_objects::UserId,
};
use sqlx::PgPool;
use crate::models::{
DirectorCountRow, GenreCountRow, MonthlyRatingRow, RatingDistRow, UserTotalsRow,
WatchMediumCountRow,
ActorAppearanceRow, DirectorCountRow, GenreCountRow, MonthlyRatingRow, RatingDistRow,
UserTotalsRow, WatchMediumCountRow,
};
use adapter_common::format_year_month;
@@ -100,7 +102,7 @@ impl StatsRepository for PostgresStatsRepository {
async fn get_user_trends(&self, user_id: &UserId) -> Result<UserTrends, DomainError> {
let uid = user_id.value().to_string();
let (rating_rows, director_rows, genre_rows, rating_dist_rows, medium_rows) =
let (rating_rows, director_rows, genre_rows, rating_dist_rows, medium_rows, actor_rows) =
tokio::try_join!(
sqlx::query_as::<_, MonthlyRatingRow>(
"SELECT to_char(watched_at AT TIME ZONE 'UTC', 'YYYY-MM') AS month,
@@ -152,6 +154,16 @@ impl StatsRepository for PostgresStatsRepository {
ORDER BY COUNT(*) DESC"
)
.bind(&uid)
.fetch_all(&self.pool),
sqlx::query_as::<_, ActorAppearanceRow>(
"SELECT mc.tmdb_person_id, mc.name, mc.profile_path,
mc.billing_order, mc.movie_id
FROM reviews r
INNER JOIN movie_cast mc ON mc.movie_id = r.movie_id
WHERE r.user_id = $1
GROUP BY mc.tmdb_person_id, mc.movie_id, mc.name, mc.profile_path, mc.billing_order"
)
.bind(&uid)
.fetch_all(&self.pool)
)
.map_err(adapter_common::map_sqlx_error)?;
@@ -201,6 +213,19 @@ impl StatsRepository for PostgresStatsRepository {
})
.collect();
let top_actors = compute_top_actors(
actor_rows
.into_iter()
.map(|r| ActorAppearance {
tmdb_person_id: r.tmdb_person_id as u64,
name: r.name,
profile_path: r.profile_path,
billing_order: r.billing_order as u32,
movie_id: r.movie_id,
})
.collect(),
);
Ok(UserTrends {
monthly_ratings,
top_directors,
@@ -208,6 +233,7 @@ impl StatsRepository for PostgresStatsRepository {
top_genres,
rating_distribution,
watch_medium_distribution,
top_actors,
})
}
}

View File

@@ -282,6 +282,15 @@ pub(crate) struct WatchMediumCountRow {
pub count: i64,
}
#[derive(sqlx::FromRow)]
pub(crate) struct ActorAppearanceRow {
pub tmdb_person_id: i64,
pub name: String,
pub profile_path: Option<String>,
pub billing_order: i32,
pub movie_id: String,
}
#[derive(sqlx::FromRow)]
pub(crate) struct WatchlistRow {
pub id: String,

View File

@@ -1,15 +1,17 @@
use async_trait::async_trait;
use domain::{
errors::DomainError,
models::{DirectorStat, MonthlyRating, UserStats, UserTrends},
models::{
ActorAppearance, DirectorStat, MonthlyRating, UserStats, UserTrends, compute_top_actors,
},
ports::StatsRepository,
value_objects::UserId,
};
use sqlx::SqlitePool;
use crate::models::{
DirectorCountRow, GenreCountRow, MonthlyRatingRow, RatingDistRow, UserTotalsRow,
WatchMediumCountRow,
ActorAppearanceRow, DirectorCountRow, GenreCountRow, MonthlyRatingRow, RatingDistRow,
UserTotalsRow, WatchMediumCountRow,
};
pub struct SqliteStatsRepository {
@@ -101,7 +103,7 @@ impl StatsRepository for SqliteStatsRepository {
async fn get_user_trends(&self, user_id: &UserId) -> Result<UserTrends, DomainError> {
let uid = user_id.value().to_string();
let (rating_rows, director_rows, genre_rows, rating_dist_rows, medium_rows) =
let (rating_rows, director_rows, genre_rows, rating_dist_rows, medium_rows, actor_rows) =
tokio::try_join!(
sqlx::query_as::<_, MonthlyRatingRow>(
"SELECT strftime('%Y-%m', watched_at) AS month,
@@ -154,6 +156,16 @@ impl StatsRepository for SqliteStatsRepository {
ORDER BY COUNT(*) DESC",
)
.bind(&uid)
.fetch_all(&self.pool),
sqlx::query_as::<_, ActorAppearanceRow>(
"SELECT mc.tmdb_person_id, mc.name, mc.profile_path,
mc.billing_order, mc.movie_id
FROM reviews r
INNER JOIN movie_cast mc ON mc.movie_id = r.movie_id
WHERE r.user_id = ?
GROUP BY mc.tmdb_person_id, mc.movie_id",
)
.bind(&uid)
.fetch_all(&self.pool)
)
.map_err(adapter_common::map_sqlx_error)?;
@@ -203,6 +215,19 @@ impl StatsRepository for SqliteStatsRepository {
})
.collect();
let top_actors = compute_top_actors(
actor_rows
.into_iter()
.map(|r| ActorAppearance {
tmdb_person_id: r.tmdb_person_id as u64,
name: r.name,
profile_path: r.profile_path,
billing_order: r.billing_order as u32,
movie_id: r.movie_id,
})
.collect(),
);
Ok(UserTrends {
monthly_ratings,
top_directors,
@@ -210,6 +235,7 @@ impl StatsRepository for SqliteStatsRepository {
top_genres,
rating_distribution,
watch_medium_distribution,
top_actors,
})
}
}