HIGH: fix watch_medium data-loss bug, standardize error handling on ApiError, fix dep direction (rss/template-askama no longer dep on application), extract ImageFetcher port (remove reqwest from app layer), move event construction from save_review to use case, extract infra-wiring crate (DbPool/EventBusBackend dedup), deduplicate presentation helpers (encode_error, export streaming, multipart parsing) MEDIUM: split LocalApContentQuery god-trait 10→3 methods, dedup movie resolution orchestration, add RemoteActorDto/PersonDto mappers, move AppConfig to infra-wiring, fix SocialQueryPort Uuid→UserId, replace stringly-typed api-types with domain enums, move count_reviews_in_year to StatsRepository, dedup event publisher cfg blocks, extract should_enrich, move group_by_month to application, dedup count_local_posts, add FederationFlags Default, TUI input helper + ShowError rename + typed auth errors, api-types cleanup (UserSettingsDto/UserProfileBase/PreviewRowData) 102 files changed, -681 lines net
189 lines
6.8 KiB
Rust
189 lines
6.8 KiB
Rust
use axum::{
|
|
extract::{Path, Query, State},
|
|
http::StatusCode,
|
|
response::{IntoResponse, Redirect},
|
|
};
|
|
|
|
use application::{
|
|
person::{deps::GetPersonDeps, get as get_person, get_credits as get_person_credits},
|
|
search::execute as search_uc,
|
|
};
|
|
use domain::models::{PersonId, collections::PageParams};
|
|
|
|
use crate::state::AppState;
|
|
use api_types::search::{
|
|
CastCreditDto, CrewCreditDto, MovieSearchHitDto, PaginatedMovieHits, PaginatedPersonHits,
|
|
PersonCreditsDto, PersonSearchHitDto, SearchQueryParams, SearchResponse,
|
|
};
|
|
|
|
// ── API ──────────────────────────────────────────────────────────────────────
|
|
|
|
#[utoipa::path(
|
|
get, path = "/api/v1/search",
|
|
params(api_types::search::SearchQueryParams),
|
|
responses(
|
|
(status = 200, body = api_types::search::SearchResponse),
|
|
),
|
|
tag = "search",
|
|
)]
|
|
pub async fn get_search(
|
|
State(state): State<AppState>,
|
|
Query(params): Query<SearchQueryParams>,
|
|
) -> impl IntoResponse {
|
|
let query = domain::models::SearchQuery {
|
|
text: params.q,
|
|
filters: domain::models::SearchFilters {
|
|
genre: params.genre,
|
|
year: params.year,
|
|
person_id: params.person_id.map(PersonId::from_uuid),
|
|
department: params.department,
|
|
language: params.language,
|
|
},
|
|
page: PageParams {
|
|
limit: params.limit.unwrap_or(5),
|
|
offset: params.offset.unwrap_or(0),
|
|
},
|
|
};
|
|
|
|
match search_uc::execute(state.app_ctx.repos.search_port.clone(), query).await {
|
|
Ok(results) => axum::Json(SearchResponse {
|
|
movies: PaginatedMovieHits {
|
|
items: results
|
|
.movies
|
|
.items
|
|
.iter()
|
|
.map(|h| MovieSearchHitDto {
|
|
movie_id: h.movie_id.value(),
|
|
title: h.title.clone(),
|
|
release_year: h.release_year,
|
|
director: h.director.clone(),
|
|
poster_path: h.poster_path.clone(),
|
|
genres: h.genres.clone(),
|
|
})
|
|
.collect(),
|
|
total_count: results.movies.total_count,
|
|
limit: results.movies.limit,
|
|
offset: results.movies.offset,
|
|
},
|
|
people: PaginatedPersonHits {
|
|
items: results
|
|
.people
|
|
.items
|
|
.iter()
|
|
.map(|h| PersonSearchHitDto {
|
|
person_id: h.person_id.value(),
|
|
name: h.name.clone(),
|
|
known_for_department: h.known_for_department.clone(),
|
|
profile_path: h.profile_path.clone(),
|
|
known_for_titles: h.known_for_titles.clone(),
|
|
})
|
|
.collect(),
|
|
total_count: results.people.total_count,
|
|
limit: results.people.limit,
|
|
offset: results.people.offset,
|
|
},
|
|
})
|
|
.into_response(),
|
|
Err(e) => crate::errors::domain_error_response(e),
|
|
}
|
|
}
|
|
|
|
#[utoipa::path(
|
|
get, path = "/api/v1/people/{id}",
|
|
params(("id" = Uuid, Path, description = "Person ID")),
|
|
responses(
|
|
(status = 200, body = api_types::search::PersonDto),
|
|
(status = 404, description = "Person not found"),
|
|
),
|
|
tag = "search",
|
|
)]
|
|
pub async fn get_person_handler(
|
|
State(state): State<AppState>,
|
|
Path(id): Path<uuid::Uuid>,
|
|
) -> impl IntoResponse {
|
|
let deps = GetPersonDeps {
|
|
person_query: state.app_ctx.repos.person_query.clone(),
|
|
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
|
};
|
|
match get_person::execute(&deps, PersonId::from_uuid(id)).await {
|
|
Ok(Some(person)) => {
|
|
axum::Json(crate::mappers::search::person_to_dto(&person)).into_response()
|
|
}
|
|
Ok(None) => StatusCode::NOT_FOUND.into_response(),
|
|
Err(e) => crate::errors::domain_error_response(e),
|
|
}
|
|
}
|
|
|
|
#[utoipa::path(
|
|
get, path = "/api/v1/people/{id}/credits",
|
|
params(("id" = Uuid, Path, description = "Person ID")),
|
|
responses(
|
|
(status = 200, body = api_types::search::PersonCreditsDto),
|
|
(status = 404, description = "Person not found"),
|
|
),
|
|
tag = "search",
|
|
)]
|
|
pub async fn get_person_credits_handler(
|
|
State(state): State<AppState>,
|
|
Path(id): Path<uuid::Uuid>,
|
|
) -> impl IntoResponse {
|
|
let deps = GetPersonDeps {
|
|
person_query: state.app_ctx.repos.person_query.clone(),
|
|
event_publisher: state.app_ctx.services.event_publisher.clone(),
|
|
};
|
|
match get_person_credits::execute(&deps, PersonId::from_uuid(id)).await {
|
|
Ok(credits) => axum::Json(PersonCreditsDto {
|
|
person: crate::mappers::search::person_to_dto(&credits.person),
|
|
cast: credits
|
|
.cast
|
|
.iter()
|
|
.map(|c| CastCreditDto {
|
|
movie_id: c.movie_id.value(),
|
|
title: c.title.clone(),
|
|
release_year: c.release_year,
|
|
character: c.character.clone(),
|
|
poster_path: c.poster_path.clone(),
|
|
})
|
|
.collect(),
|
|
crew: credits
|
|
.crew
|
|
.iter()
|
|
.map(|c| CrewCreditDto {
|
|
movie_id: c.movie_id.value(),
|
|
title: c.title.clone(),
|
|
release_year: c.release_year,
|
|
job: c.job.clone(),
|
|
department: c.department.clone(),
|
|
poster_path: c.poster_path.clone(),
|
|
})
|
|
.collect(),
|
|
})
|
|
.into_response(),
|
|
Err(e) => crate::errors::domain_error_response(e),
|
|
}
|
|
}
|
|
|
|
pub async fn post_reindex_search(
|
|
State(state): State<AppState>,
|
|
_admin: crate::extractors::AdminApiUser,
|
|
) -> impl IntoResponse {
|
|
let event = domain::events::DomainEvent::SearchReindexRequested;
|
|
match state.app_ctx.services.event_publisher.publish(&event).await {
|
|
Ok(()) => StatusCode::ACCEPTED,
|
|
Err(e) => {
|
|
tracing::error!("failed to publish reindex event: {:?}", e);
|
|
StatusCode::INTERNAL_SERVER_ERROR
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── HTML ─────────────────────────────────────────────────────────────────────
|
|
|
|
pub async fn get_tag(Path(tag): Path<String>) -> impl IntoResponse {
|
|
if tag.eq_ignore_ascii_case("moviesdiary") {
|
|
Redirect::temporary("/")
|
|
} else {
|
|
Redirect::temporary(&format!("/?search={}", tag))
|
|
}
|
|
}
|