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

@@ -2,20 +2,20 @@ use std::sync::Arc;
use anyhow::Context;
use domain::ports::{
ImageRefCommand, ImageRefQuery, ImportSessionRepository, LocalApContentQuery,
MovieDeduplicator, MovieProfileRepository, MovieRepository, PersonCommand, PersonQuery,
SearchCommand, UserRepository, WatchEventRepository,
DiaryRepository, GoalRepository, ImageRefCommand, ImageRefQuery, ImportSessionRepository,
LocalApContentQuery, MovieDeduplicator, MovieProfileRepository, MovieRepository, PersonCommand,
PersonQuery, ReviewRepository, SearchCommand, StatsRepository, UserRepository,
WatchEventRepository,
};
pub enum DbPool {
#[cfg(feature = "sqlite")]
Sqlite(sqlx::SqlitePool),
#[cfg(feature = "postgres")]
Postgres(sqlx::PgPool),
}
pub use infra_wiring::DbPool;
pub struct WorkerDbOutput {
pub movie: Arc<dyn MovieRepository>,
pub review: Arc<dyn ReviewRepository>,
pub diary: Arc<dyn DiaryRepository>,
pub stats: Arc<dyn StatsRepository>,
pub goal: Arc<dyn GoalRepository>,
pub user: Arc<dyn UserRepository>,
pub import_session: Arc<dyn ImportSessionRepository>,
pub movie_profile: Arc<dyn MovieProfileRepository>,
@@ -50,6 +50,10 @@ pub async fn connect(database_url: &str, backend: &str) -> anyhow::Result<Worker
Arc::new(postgres::PostgresWatchEventRepository::new(w.pool.clone()));
Ok(WorkerDbOutput {
movie: w.movie,
review: w.review,
diary: w.diary,
stats: w.stats,
goal: w.goal,
user: w.user,
import_session: w.import_session,
movie_profile: w.movie_profile,
@@ -84,6 +88,10 @@ pub async fn connect(database_url: &str, backend: &str) -> anyhow::Result<Worker
Arc::new(sqlite::SqliteWatchEventRepository::new(w.pool.clone()));
Ok(WorkerDbOutput {
movie: w.movie,
review: w.review,
diary: w.diary,
stats: w.stats,
goal: w.goal,
user: w.user,
import_session: w.import_session,
movie_profile: w.movie_profile,

View File

@@ -5,31 +5,7 @@ use anyhow::Context;
use domain::ports::{EventConsumer, EventPublisher};
use crate::db::DbPool;
#[derive(Clone, Copy)]
pub enum EventBusBackend {
Db,
#[cfg(feature = "nats")]
Nats,
}
impl EventBusBackend {
pub fn from_env() -> anyhow::Result<Self> {
match std::env::var("EVENT_BUS_BACKEND")
.unwrap_or_else(|_| "db".to_string())
.as_str()
{
"db" => Ok(Self::Db),
#[cfg(feature = "nats")]
"nats" => Ok(Self::Nats),
#[cfg(not(feature = "nats"))]
"nats" => {
anyhow::bail!("EVENT_BUS_BACKEND=nats requires the nats feature to be compiled in")
}
other => anyhow::bail!("unknown EVENT_BUS_BACKEND={other}, expected 'db' or 'nats'"),
}
}
}
use infra_wiring::EventBusBackend;
pub async fn create(
db_pool: &DbPool,

View File

@@ -38,8 +38,23 @@ async fn main() -> anyhow::Result<()> {
let image_ref_query = Arc::clone(&db.image_ref_query);
#[cfg(feature = "federation")]
let (fed_ap_content, fed_user_repo, base_url, allow_registration) = (
let (
fed_ap_content,
fed_movie_repo,
fed_review_repo,
fed_diary_repo,
fed_goal_repo,
fed_stats_repo,
fed_user_repo,
base_url,
allow_registration,
) = (
Arc::clone(&db.ap_content),
Arc::clone(&db.movie),
Arc::clone(&db.review),
Arc::clone(&db.diary),
Arc::clone(&db.goal),
Arc::clone(&db.stats),
Arc::clone(&db.user),
app_config.base_url.clone(),
app_config.allow_registration,
@@ -93,6 +108,7 @@ async fn main() -> anyhow::Result<()> {
Ok(client) => {
tracing::info!("TMDb enrichment enabled");
let client = Arc::new(client);
let image_fetcher = poster_fetcher::create_image_fetcher()?;
let handler = Arc::new(application::movies::MovieEnrichmentHandler::new(
Arc::clone(&client) as Arc<dyn MovieEnrichmentClient>,
Arc::clone(&movie),
@@ -100,6 +116,7 @@ async fn main() -> anyhow::Result<()> {
Arc::clone(&person_command),
Arc::clone(&search_command),
Arc::clone(&object_storage),
image_fetcher,
)) as Arc<dyn EventHandler>;
let person_enrichment_arc = Arc::clone(&client) as Arc<dyn PersonEnrichmentClient>;
let person_handler = Arc::new(application::person::PersonEnrichmentHandler::new(
@@ -189,48 +206,38 @@ async fn main() -> anyhow::Result<()> {
&object_storage,
))) as Arc<dyn EventHandler>;
#[cfg(not(feature = "federation"))]
{
let search_cleanup = Arc::new(SearchCleanupHandler::new(
Arc::clone(&search_command),
Arc::clone(&person_query),
)) as Arc<dyn EventHandler>;
let discovery_indexer = Arc::new(MovieDiscoveryIndexer::new(
Arc::clone(&movie),
Arc::clone(&search_command),
)) as Arc<dyn EventHandler>;
let wrapup_handler =
Arc::new(application::wrapup::event_handler::WrapUpEventHandler::new(
Arc::clone(&wrapup_repo),
Arc::clone(&event_publisher),
Arc::clone(&wrapup_stats),
)) as Arc<dyn EventHandler>;
let reindex_handler = Arc::new(SearchReindexHandler::new(ReindexSearchDeps {
movie: Arc::clone(&movie),
movie_profile: Arc::clone(&movie_profile),
search_command: Arc::clone(&search_command),
person_command: Arc::clone(&person_command),
person_query: Arc::clone(&person_query),
})) as Arc<dyn EventHandler>;
let mut h = vec![
poster,
cleanup,
search_cleanup,
discovery_indexer,
wrapup_handler,
reindex_handler,
];
if let Some(e) = enrichment_handler {
h.push(e);
}
if let Some(e) = person_enrichment_handler {
h.push(e);
}
if let Some((ref conv_handler, _)) = conversion {
h.push(Arc::clone(conv_handler));
}
h
}
let search_cleanup = Arc::new(SearchCleanupHandler::new(
Arc::clone(&search_command),
Arc::clone(&person_query),
)) as Arc<dyn EventHandler>;
let discovery_indexer = Arc::new(MovieDiscoveryIndexer::new(
Arc::clone(&movie),
Arc::clone(&search_command),
)) as Arc<dyn EventHandler>;
let wrapup_handler = Arc::new(application::wrapup::event_handler::WrapUpEventHandler::new(
Arc::clone(&wrapup_repo),
Arc::clone(&event_publisher),
Arc::clone(&wrapup_stats),
)) as Arc<dyn EventHandler>;
let reindex_handler = Arc::new(SearchReindexHandler::new(ReindexSearchDeps {
movie: Arc::clone(&movie),
movie_profile: Arc::clone(&movie_profile),
search_command: Arc::clone(&search_command),
person_command: Arc::clone(&person_command),
person_query: Arc::clone(&person_query),
})) as Arc<dyn EventHandler>;
let mut h = vec![
poster,
cleanup,
search_cleanup,
discovery_indexer,
wrapup_handler,
reindex_handler,
];
#[cfg(feature = "federation")]
{
@@ -243,6 +250,11 @@ async fn main() -> anyhow::Result<()> {
remote_watchlist_repo: fed_remote_watchlist_repo,
remote_goal_repo: Arc::clone(&remote_goal),
local_ap_content: fed_ap_content,
movie_repo: fed_movie_repo,
review_repo: fed_review_repo,
diary_repo: fed_diary_repo,
goal_repo: fed_goal_repo,
stats_repo: fed_stats_repo,
user_repo: fed_user_repo,
base_url,
allow_registration,
@@ -251,54 +263,24 @@ async fn main() -> anyhow::Result<()> {
})
.await?;
let ap_event_handler = ap_wire.event_handler;
let backfill = Arc::new(follow_backfill_handler::FollowBackfillHandler {
ap_service: ap_wire.service,
}) as Arc<dyn EventHandler>;
let search_cleanup = Arc::new(SearchCleanupHandler::new(
Arc::clone(&search_command),
Arc::clone(&person_query),
)) as Arc<dyn EventHandler>;
let discovery_indexer = Arc::new(MovieDiscoveryIndexer::new(
Arc::clone(&movie),
Arc::clone(&search_command),
)) as Arc<dyn EventHandler>;
tracing::info!("federation event handler registered");
let wrapup_handler =
Arc::new(application::wrapup::event_handler::WrapUpEventHandler::new(
Arc::clone(&wrapup_repo),
Arc::clone(&event_publisher),
Arc::clone(&wrapup_stats),
)) as Arc<dyn EventHandler>;
let reindex_handler = Arc::new(SearchReindexHandler::new(ReindexSearchDeps {
movie: Arc::clone(&movie),
movie_profile: Arc::clone(&movie_profile),
search_command: Arc::clone(&search_command),
person_command: Arc::clone(&person_command),
person_query: Arc::clone(&person_query),
})) as Arc<dyn EventHandler>;
let mut h = vec![
poster,
cleanup,
ap_event_handler,
backfill,
search_cleanup,
discovery_indexer,
wrapup_handler,
reindex_handler,
];
if let Some(e) = enrichment_handler {
h.push(e);
}
if let Some(e) = person_enrichment_handler {
h.push(e);
}
if let Some((ref conv_handler, _)) = conversion {
h.push(Arc::clone(conv_handler));
}
h
h.push(ap_wire.event_handler);
h.push(Arc::new(follow_backfill_handler::FollowBackfillHandler {
ap_service: ap_wire.service,
}) as Arc<dyn EventHandler>);
}
if let Some(e) = enrichment_handler {
h.push(e);
}
if let Some(e) = person_enrichment_handler {
h.push(e);
}
if let Some((ref conv_handler, _)) = conversion {
h.push(Arc::clone(conv_handler));
}
h
};
// ── Run ───────────────────────────────────────────────────────────────────