collapse 20 pass-through use cases; handlers call ports directly

delete get/list/list_by_owner channels, get_settings/activity_log admin,
get_item/get_sync_status/list_collections/list_seasons/list_shows/list_genres library,
get/list/delete providers, get/list/patch_label config_snapshots,
get_active/list_history/delete_after schedule — all single-delegation.

remove ChannelQueryDeps, LibraryQueryDeps, deleted query/command structs.
add direct port fields to AppState. update MCP crate accordingly.
This commit is contained in:
2026-07-12 07:18:26 +02:00
parent a6558e15b2
commit e2393be635
68 changed files with 191 additions and 1589 deletions

View File

@@ -60,10 +60,6 @@ async fn main() -> anyhow::Result<()> {
event_publisher: event_publisher.clone(),
});
let channel_query_deps = Arc::new(application::channels::ChannelQueryDeps {
channel_query: wire.channel_query.clone(),
});
let schedule_deps = Arc::new(application::schedule::ScheduleDeps {
schedule_engine,
channel_query: wire.channel_query.clone(),
@@ -73,15 +69,24 @@ async fn main() -> anyhow::Result<()> {
provider_registry: provider_registry.clone(),
});
let library_query_deps = Arc::new(application::library::LibraryQueryDeps {
let library_sync: Arc<dyn domain::ports::LibrarySyncAdapter> =
Arc::new(NoopLibrarySync);
let library_command_deps = Arc::new(application::library::LibraryCommandDeps {
library_command: wire.library_command.clone(),
library_query: wire.library_query.clone(),
library_sync,
provider_registry: provider_registry.clone(),
event_publisher: event_publisher.clone(),
});
let server = KTvMcpServer {
channel_cmd_deps,
channel_query_deps,
channel_query: wire.channel_query.clone(),
schedule_deps,
library_query_deps,
schedule_query: wire.schedule_query.clone(),
library_query: wire.library_query.clone(),
library_command_deps,
owner_id,
};
@@ -103,6 +108,7 @@ struct WireOutput {
channel_query: Arc<dyn domain::ports::ChannelQuery>,
schedule_command: Arc<dyn domain::ports::ScheduleCommand>,
schedule_query: Arc<dyn domain::ports::ScheduleQuery>,
library_command: Arc<dyn domain::ports::LibraryCommand>,
library_query: Arc<dyn domain::ports::LibraryQuery>,
}
@@ -116,6 +122,7 @@ fn wire_repositories(pool: &DbPool) -> anyhow::Result<WireOutput> {
channel_query: w.channel_query,
schedule_command: w.schedule_command,
schedule_query: w.schedule_query,
library_command: w.library_command,
library_query: w.library_query,
})
}
@@ -325,3 +332,16 @@ impl IProviderRegistry for SimpleProviderRegistry {
provider.list_genres(content_type).await
}
}
struct NoopLibrarySync;
#[async_trait::async_trait]
impl domain::ports::LibrarySyncAdapter for NoopLibrarySync {
async fn sync_provider(
&self,
_provider: &dyn IMediaProvider,
provider_id: &str,
) -> domain::LibrarySyncResult {
domain::LibrarySyncResult::with_error(provider_id, 0, "MCP does not support sync")
}
}

View File

@@ -1,8 +1,8 @@
use std::sync::Arc;
use application::{
channels::{ChannelCommandDeps, ChannelQueryDeps},
library::LibraryQueryDeps,
channels::ChannelCommandDeps,
library::LibraryCommandDeps,
schedule::ScheduleDeps,
};
use rmcp::{
@@ -21,9 +21,11 @@ const SERVER_NAME: &str = "k-tv-mcp";
#[derive(Clone)]
pub struct KTvMcpServer {
pub channel_cmd_deps: Arc<ChannelCommandDeps>,
pub channel_query_deps: Arc<ChannelQueryDeps>,
pub channel_query: Arc<dyn domain::ports::ChannelQuery>,
pub schedule_deps: Arc<ScheduleDeps>,
pub library_query_deps: Arc<LibraryQueryDeps>,
pub schedule_query: Arc<dyn domain::ports::ScheduleQuery>,
pub library_query: Arc<dyn domain::ports::LibraryQuery>,
pub library_command_deps: Arc<LibraryCommandDeps>,
pub owner_id: Uuid,
}
@@ -80,13 +82,13 @@ fn parse_uuid(s: &str) -> Result<Uuid, String> {
impl KTvMcpServer {
#[tool(description = "List all channels owned by the configured user")]
async fn list_channels(&self) -> String {
channels::list_channels(&self.channel_query_deps, self.owner_id).await
channels::list_channels(&self.channel_query, self.owner_id).await
}
#[tool(description = "Get a channel by UUID")]
async fn get_channel(&self, #[tool(aggr)] p: GetChannelParams) -> String {
match parse_uuid(&p.id) {
Ok(id) => channels::get_channel(&self.channel_query_deps, id).await,
Ok(id) => channels::get_channel(&self.channel_query, id).await,
Err(e) => e,
}
}
@@ -143,7 +145,7 @@ impl KTvMcpServer {
#[tool(description = "Get the currently active schedule for a channel (returns null if none)")]
async fn get_active_schedule(&self, #[tool(aggr)] p: ChannelIdParam) -> String {
match parse_uuid(&p.channel_id) {
Ok(id) => schedule::get_active_schedule(&self.schedule_deps, id).await,
Ok(id) => schedule::get_active_schedule(&self.schedule_query, id).await,
Err(e) => e,
}
}
@@ -160,14 +162,14 @@ impl KTvMcpServer {
#[tool(description = "List media collections/libraries available in the library")]
async fn list_collections(&self) -> String {
library::list_collections(&self.library_query_deps).await
library::list_collections(&self.library_query).await
}
#[tool(
description = "List genres available in the library, optionally filtered by content type (movie/episode/short)"
)]
async fn list_genres(&self, #[tool(aggr)] p: ListGenresParams) -> String {
library::list_genres(&self.library_query_deps, p.content_type).await
library::list_genres(&self.library_query, p.content_type).await
}
#[tool(
@@ -175,7 +177,7 @@ impl KTvMcpServer {
)]
async fn search_media(&self, #[tool(aggr)] p: SearchMediaParams) -> String {
library::search_media(
&self.library_query_deps,
&self.library_command_deps,
p.content_type,
p.genres.unwrap_or_default(),
p.search_term,

View File

@@ -1,31 +1,27 @@
use std::sync::Arc;
use application::channels::{
ChannelCommandDeps, ChannelQueryDeps, CreateChannelCommand, DeleteChannelCommand,
GetChannelQuery, ListByOwnerQuery, UpdateChannelCommand,
ChannelCommandDeps, CreateChannelCommand, DeleteChannelCommand, UpdateChannelCommand,
};
use uuid::Uuid;
use crate::error::{domain_err, ok_json};
pub async fn list_channels(
query_deps: &Arc<ChannelQueryDeps>,
channel_query: &Arc<dyn domain::ports::ChannelQuery>,
owner_id: Uuid,
) -> String {
let query = ListByOwnerQuery {
owner_id: owner_id.into(),
};
match application::channels::list_by_owner::execute(query_deps, query).await {
match channel_query.find_by_owner(owner_id.into()).await {
Ok(channels) => ok_json(&channels),
Err(e) => domain_err(e),
}
}
pub async fn get_channel(query_deps: &Arc<ChannelQueryDeps>, id: Uuid) -> String {
let query = GetChannelQuery {
channel_id: id.into(),
};
match application::channels::get::execute(query_deps, query).await {
pub async fn get_channel(
channel_query: &Arc<dyn domain::ports::ChannelQuery>,
id: Uuid,
) -> String {
match channel_query.find_by_id(id.into()).await {
Ok(Some(channel)) => ok_json(&channel),
Ok(None) => serde_json::json!({"error": "Channel not found"}).to_string(),
Err(e) => domain_err(e),

View File

@@ -1,8 +1,6 @@
use std::sync::Arc;
use application::library::{
LibraryQueryDeps, ListCollectionsQuery, ListGenresQuery, SearchItemsQuery,
};
use application::library::SearchItemsQuery;
use serde::Serialize;
use crate::error::{domain_err, ok_json};
@@ -48,9 +46,8 @@ fn content_type_to_str(ct: &domain::ContentType) -> &'static str {
}
}
pub async fn list_collections(deps: &Arc<LibraryQueryDeps>) -> String {
let query = ListCollectionsQuery { provider_id: None };
match application::library::list_collections::execute(deps, query).await {
pub async fn list_collections(library_query: &Arc<dyn domain::ports::LibraryQuery>) -> String {
match library_query.list_collections(None).await {
Ok(cols) => {
let dtos: Vec<CollectionDto> = cols
.into_iter()
@@ -66,19 +63,26 @@ pub async fn list_collections(deps: &Arc<LibraryQueryDeps>) -> String {
}
}
pub async fn list_genres(deps: &Arc<LibraryQueryDeps>, content_type: Option<String>) -> String {
let query = ListGenresQuery {
content_type,
provider_id: None,
pub async fn list_genres(
library_query: &Arc<dyn domain::ports::LibraryQuery>,
content_type: Option<String>,
) -> String {
let ct = match content_type
.as_deref()
.map(application::library::parse_content_type)
.transpose()
{
Ok(ct) => ct,
Err(e) => return domain_err(e),
};
match application::library::list_genres::execute(deps, query).await {
match library_query.list_genres(ct.as_ref(), None).await {
Ok(genres) => ok_json(&genres),
Err(e) => domain_err(e),
}
}
pub async fn search_media(
deps: &Arc<LibraryQueryDeps>,
library_command_deps: &Arc<application::library::LibraryCommandDeps>,
content_type: Option<String>,
genres: Vec<String>,
search_term: Option<String>,
@@ -97,7 +101,7 @@ pub async fn search_media(
offset: 0,
limit: DEFAULT_SEARCH_LIMIT,
};
match application::library::search::execute(deps, query).await {
match application::library::search::execute(library_command_deps, query).await {
Ok((items, total)) => {
let dtos: Vec<LibraryItemDto> = items
.into_iter()

View File

@@ -1,9 +1,11 @@
use std::sync::Arc;
use application::schedule::{
GenerateScheduleCommand, GetActiveScheduleQuery, GetCurrentBroadcastQuery, ScheduleDeps,
GenerateScheduleCommand, GetCurrentBroadcastQuery, ScheduleDeps,
};
use chrono::Utc;
use domain::ScheduledSlot;
use domain::value_objects::ChannelId;
use serde::Serialize;
use uuid::Uuid;
@@ -23,9 +25,12 @@ pub async fn generate_schedule(deps: &Arc<ScheduleDeps>, channel_id: Uuid) -> St
}
}
pub async fn get_active_schedule(deps: &Arc<ScheduleDeps>, channel_id: Uuid) -> String {
let query = GetActiveScheduleQuery { channel_id };
match application::schedule::get_active::execute(deps, query).await {
pub async fn get_active_schedule(
schedule_query: &Arc<dyn domain::ports::ScheduleQuery>,
channel_id: Uuid,
) -> String {
let cid = ChannelId::from(channel_id);
match schedule_query.find_active(cid, Utc::now()).await {
Ok(Some(schedule)) => ok_json(&schedule),
Ok(None) => "null".to_string(),
Err(e) => domain_err(e),