refactor: fix HIGH+MEDIUM architectural violations from code review

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
This commit is contained in:
2026-07-10 02:08:39 +02:00
parent 26152660bb
commit 12da356a40
110 changed files with 1399 additions and 1867 deletions

View File

@@ -6,7 +6,7 @@ use domain::{
events::DomainEvent,
models::MovieProfile,
ports::{
EventHandler, MovieEnrichmentClient, MovieProfileRepository, MovieRepository,
EventHandler, ImageFetcher, MovieEnrichmentClient, MovieProfileRepository, MovieRepository,
ObjectStorage, PersonCommand, SearchCommand,
},
};
@@ -22,7 +22,7 @@ pub struct MovieEnrichmentHandler {
person_command: Arc<dyn PersonCommand>,
search_command: Arc<dyn SearchCommand>,
object_storage: Arc<dyn ObjectStorage>,
http: reqwest::Client,
image_fetcher: Arc<dyn ImageFetcher>,
}
impl MovieEnrichmentHandler {
@@ -33,6 +33,7 @@ impl MovieEnrichmentHandler {
person_command: Arc<dyn PersonCommand>,
search_command: Arc<dyn SearchCommand>,
object_storage: Arc<dyn ObjectStorage>,
image_fetcher: Arc<dyn ImageFetcher>,
) -> Self {
Self {
enrichment_client,
@@ -41,7 +42,7 @@ impl MovieEnrichmentHandler {
person_command,
search_command,
object_storage,
http: reqwest::Client::new(),
image_fetcher,
}
}
@@ -55,15 +56,13 @@ impl MovieEnrichmentHandler {
continue;
}
let url = format!("https://image.tmdb.org/t/p/w185{path}");
match self.http.get(&url).send().await {
Ok(resp) if resp.status().is_success() => {
if let Ok(bytes) = resp.bytes().await
&& let Err(e) = self.object_storage.store(&key, &bytes).await
{
match self.image_fetcher.fetch_image(&url).await {
Ok(bytes) => {
if let Err(e) = self.object_storage.store(&key, &bytes).await {
tracing::debug!("cast photo store failed for {path}: {e}");
}
}
_ => tracing::debug!("cast photo download failed for {path}"),
Err(_) => tracing::debug!("cast photo download failed for {path}"),
}
}
}

View File

@@ -9,6 +9,7 @@ pub mod merge_duplicates;
pub mod queries;
pub mod reindex_search;
pub mod request_enrichment;
pub mod resolve;
pub mod search_cleanup;
pub mod sync_poster;

View File

@@ -0,0 +1,51 @@
use domain::{
errors::DomainError,
events::DomainEvent,
models::Movie,
ports::{EventPublisher, MetadataClient, MovieRepository},
value_objects::MovieId,
};
use crate::diary::commands::MovieInput;
use crate::diary::movie_resolver::{MovieResolver, MovieResolverDeps};
/// Resolves a movie from input, persists it, and publishes `MovieDiscovered` if new.
///
/// Returns `(movie, is_new_movie)`.
pub async fn resolve_and_persist_movie(
input: &MovieInput,
movie_repo: &dyn MovieRepository,
metadata_client: &dyn MetadataClient,
event_publisher: &dyn EventPublisher,
) -> Result<(Movie, bool), DomainError> {
let (movie, is_new) = if let Some(id) = input.movie_id {
let movie_id = MovieId::from_uuid(id);
let movie = movie_repo
.get_movie_by_id(&movie_id)
.await?
.ok_or_else(|| DomainError::NotFound(format!("Movie {id}")))?;
(movie, false)
} else {
let deps = MovieResolverDeps {
repository: movie_repo,
metadata_client,
};
MovieResolver::default_pipeline()
.resolve(input, &deps)
.await?
};
if is_new {
movie_repo.upsert_movie(&movie).await?;
if let Some(ext_id) = movie.external_metadata_id() {
let _ = event_publisher
.publish(&DomainEvent::MovieDiscovered {
movie_id: movie.id().clone(),
external_metadata_id: ext_id.clone(),
})
.await;
}
}
Ok((movie, is_new))
}