feat: search reindex, worker improvements, person IDs, user display names
- add admin POST /api/v1/admin/reindex-search endpoint + event-driven handler - backfill persons from movie_cast/movie_crew into persons table - paginate person list_page/backfill_from_credits_batch to cap memory - concurrent worker event dispatch with semaphore (max 8) - graceful worker shutdown (drain in-flight tasks on SIGINT) - always ack events, log handler errors as warnings (no infinite retry) - NATS ack_wait 600s, AtomicBool guard against concurrent reindex - add username/display_name to UserSummaryDto and users list - add person_id to CastMemberDto/CrewMemberDto via get_movie_profile use case - add movie_id to wrapup MovieRef, person_id to wrapup PersonStat - thread tmdb_person_id through wrapup cast pipeline - add is_federated to FeedEntryDto - cap orphaned persons query with LIMIT 500 - add SPA link to classic site footer
This commit is contained in:
@@ -84,6 +84,7 @@ pub enum DomainEvent {
|
||||
WrapUpCompleted {
|
||||
wrapup_id: WrapUpId,
|
||||
},
|
||||
SearchReindexRequested,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -461,6 +461,8 @@ impl FeedEntry {
|
||||
pub struct UserSummary {
|
||||
pub user_id: UserId,
|
||||
email: Email,
|
||||
username: Username,
|
||||
display_name: Option<String>,
|
||||
pub total_movies: i64,
|
||||
pub avg_rating: Option<f64>,
|
||||
pub avatar_path: Option<String>,
|
||||
@@ -470,6 +472,8 @@ impl UserSummary {
|
||||
pub fn new(
|
||||
user_id: UserId,
|
||||
email: Email,
|
||||
username: Username,
|
||||
display_name: Option<String>,
|
||||
total_movies: i64,
|
||||
avg_rating: Option<f64>,
|
||||
avatar_path: Option<String>,
|
||||
@@ -477,6 +481,8 @@ impl UserSummary {
|
||||
Self {
|
||||
user_id,
|
||||
email,
|
||||
username,
|
||||
display_name,
|
||||
total_movies,
|
||||
avg_rating,
|
||||
avatar_path,
|
||||
@@ -485,6 +491,12 @@ impl UserSummary {
|
||||
pub fn email(&self) -> &str {
|
||||
self.email.value()
|
||||
}
|
||||
pub fn username(&self) -> &str {
|
||||
self.username.value()
|
||||
}
|
||||
pub fn display_name(&self) -> Option<&str> {
|
||||
self.display_name.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -36,6 +36,7 @@ impl DateRange {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct MovieRef {
|
||||
pub movie_id: Option<Uuid>,
|
||||
pub title: String,
|
||||
pub year: u16,
|
||||
pub runtime_minutes: Option<u32>,
|
||||
@@ -50,6 +51,7 @@ pub struct UserRef {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct PersonStat {
|
||||
pub person_id: Option<Uuid>,
|
||||
pub name: String,
|
||||
pub count: u32,
|
||||
pub avg_rating: f64,
|
||||
|
||||
@@ -332,6 +332,12 @@ pub trait ImageRefQuery: Send + Sync {
|
||||
pub trait PersonCommand: Send + Sync {
|
||||
/// Upsert a batch of persons. Uses INSERT OR REPLACE (SQLite) / ON CONFLICT DO UPDATE (Postgres).
|
||||
async fn upsert_batch(&self, persons: &[Person]) -> Result<(), DomainError>;
|
||||
/// Insert a batch of missing persons from movie_cast/movie_crew into the persons table.
|
||||
/// Returns (inserted_count, has_more).
|
||||
async fn backfill_from_credits_batch(
|
||||
&self,
|
||||
batch_size: u32,
|
||||
) -> Result<(u64, bool), DomainError>;
|
||||
}
|
||||
|
||||
/// Read port — queries persons and credits. No mutations.
|
||||
@@ -347,6 +353,7 @@ pub trait PersonQuery: Send + Sync {
|
||||
/// Returns persons who have no remaining entries in movie_cast or movie_crew.
|
||||
/// Called after movie deletion to find index entries that can be pruned.
|
||||
async fn list_orphaned_persons(&self) -> Result<Vec<PersonId>, DomainError>;
|
||||
async fn list_page(&self, limit: u32, offset: u32) -> Result<Vec<Person>, DomainError>;
|
||||
}
|
||||
|
||||
/// Read port — executes search queries. No mutations.
|
||||
@@ -519,7 +526,7 @@ pub struct WrapUpMovieRow {
|
||||
pub original_language: Option<String>,
|
||||
pub genres: Vec<String>,
|
||||
pub keywords: Vec<String>,
|
||||
pub cast_names: Vec<(String, u32)>,
|
||||
pub cast_names: Vec<(String, u32, i64)>,
|
||||
pub cast_profile_paths: Vec<Option<String>>,
|
||||
}
|
||||
|
||||
|
||||
@@ -672,6 +672,12 @@ impl PersonCommand for PanicPersonCommand {
|
||||
async fn upsert_batch(&self, _persons: &[Person]) -> Result<(), DomainError> {
|
||||
panic!("PanicPersonCommand called")
|
||||
}
|
||||
async fn backfill_from_credits_batch(
|
||||
&self,
|
||||
_batch_size: u32,
|
||||
) -> Result<(u64, bool), DomainError> {
|
||||
panic!("PanicPersonCommand called")
|
||||
}
|
||||
}
|
||||
|
||||
// ── PanicPersonQuery ──────────────────────────────────────────────────────────
|
||||
@@ -698,6 +704,10 @@ impl PersonQuery for PanicPersonQuery {
|
||||
async fn list_orphaned_persons(&self) -> Result<Vec<PersonId>, DomainError> {
|
||||
panic!("PanicPersonQuery called")
|
||||
}
|
||||
|
||||
async fn list_page(&self, _limit: u32, _offset: u32) -> Result<Vec<Person>, DomainError> {
|
||||
panic!("PanicPersonQuery called")
|
||||
}
|
||||
}
|
||||
|
||||
// ── PanicSearchPort ───────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user