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:
@@ -1,12 +0,0 @@
|
||||
use domain::models::ActivityEvent;
|
||||
use domain::DomainResult;
|
||||
|
||||
use super::deps::AdminDeps;
|
||||
use super::queries::GetActivityLogQuery;
|
||||
|
||||
pub async fn execute(
|
||||
deps: &AdminDeps,
|
||||
query: GetActivityLogQuery,
|
||||
) -> DomainResult<Vec<ActivityEvent>> {
|
||||
deps.activity_query.recent(query.limit).await
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
use domain::DomainResult;
|
||||
|
||||
use super::deps::AdminDeps;
|
||||
use super::queries::GetSettingsQuery;
|
||||
|
||||
pub async fn execute(
|
||||
deps: &AdminDeps,
|
||||
_query: GetSettingsQuery,
|
||||
) -> DomainResult<Vec<(String, String)>> {
|
||||
deps.settings_repo.get_all().await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/get_settings.rs"]
|
||||
mod tests;
|
||||
@@ -1,10 +1,6 @@
|
||||
pub mod activity_log;
|
||||
pub mod commands;
|
||||
pub mod deps;
|
||||
pub mod get_settings;
|
||||
pub mod queries;
|
||||
pub mod update_settings;
|
||||
|
||||
pub use commands::UpdateSettingsCommand;
|
||||
pub use deps::AdminDeps;
|
||||
pub use queries::{GetActivityLogQuery, GetSettingsQuery};
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
pub struct GetSettingsQuery;
|
||||
|
||||
pub struct GetActivityLogQuery {
|
||||
pub limit: u32,
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::testing::{InMemoryActivityLog, InMemoryAppSettings};
|
||||
|
||||
use crate::admin::commands::UpdateSettingsCommand;
|
||||
use crate::admin::deps::AdminDeps;
|
||||
use crate::admin::queries::GetSettingsQuery;
|
||||
use crate::admin::{get_settings, update_settings};
|
||||
|
||||
fn make_deps() -> AdminDeps {
|
||||
AdminDeps {
|
||||
settings_repo: Arc::new(InMemoryAppSettings::new()),
|
||||
activity_query: Arc::new(InMemoryActivityLog::new()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_empty_settings() {
|
||||
let deps = make_deps();
|
||||
let settings = get_settings::execute(&deps, GetSettingsQuery).await.unwrap();
|
||||
assert!(settings.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_returns_stored_settings() {
|
||||
let deps = make_deps();
|
||||
|
||||
update_settings::execute(
|
||||
&deps,
|
||||
UpdateSettingsCommand {
|
||||
settings: vec![
|
||||
("a".into(), "1".into()),
|
||||
("b".into(), "2".into()),
|
||||
],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let settings = get_settings::execute(&deps, GetSettingsQuery).await.unwrap();
|
||||
assert_eq!(settings.len(), 2);
|
||||
|
||||
let keys: Vec<&str> = settings.iter().map(|(k, _)| k.as_str()).collect();
|
||||
assert!(keys.contains(&"a"));
|
||||
assert!(keys.contains(&"b"));
|
||||
}
|
||||
@@ -7,7 +7,3 @@ pub struct ChannelCommandDeps {
|
||||
pub channel_query: Arc<dyn ChannelQuery>,
|
||||
pub event_publisher: Arc<dyn EventPublisher>,
|
||||
}
|
||||
|
||||
pub struct ChannelQueryDeps {
|
||||
pub channel_query: Arc<dyn ChannelQuery>,
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
use domain::models::Channel;
|
||||
use domain::DomainResult;
|
||||
|
||||
use super::deps::ChannelQueryDeps;
|
||||
use super::queries::GetChannelQuery;
|
||||
|
||||
pub async fn execute(deps: &ChannelQueryDeps, query: GetChannelQuery) -> DomainResult<Option<Channel>> {
|
||||
deps.channel_query.find_by_id(query.channel_id).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/get.rs"]
|
||||
mod tests;
|
||||
@@ -1,13 +0,0 @@
|
||||
use domain::models::Channel;
|
||||
use domain::DomainResult;
|
||||
|
||||
use super::deps::ChannelQueryDeps;
|
||||
use super::queries::ListChannelsQuery;
|
||||
|
||||
pub async fn execute(deps: &ChannelQueryDeps, _query: ListChannelsQuery) -> DomainResult<Vec<Channel>> {
|
||||
deps.channel_query.find_all().await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/list.rs"]
|
||||
mod tests;
|
||||
@@ -1,13 +0,0 @@
|
||||
use domain::models::Channel;
|
||||
use domain::DomainResult;
|
||||
|
||||
use super::deps::ChannelQueryDeps;
|
||||
use super::queries::ListByOwnerQuery;
|
||||
|
||||
pub async fn execute(deps: &ChannelQueryDeps, query: ListByOwnerQuery) -> DomainResult<Vec<Channel>> {
|
||||
deps.channel_query.find_by_owner(query.owner_id).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/list_by_owner.rs"]
|
||||
mod tests;
|
||||
@@ -2,15 +2,10 @@ pub mod commands;
|
||||
pub mod create;
|
||||
pub mod delete;
|
||||
pub mod deps;
|
||||
pub mod get;
|
||||
pub mod list;
|
||||
pub mod list_by_owner;
|
||||
pub mod queries;
|
||||
pub mod update;
|
||||
|
||||
pub use commands::{CreateChannelCommand, DeleteChannelCommand, UpdateChannelCommand};
|
||||
pub use deps::{ChannelCommandDeps, ChannelQueryDeps};
|
||||
pub use queries::{GetChannelQuery, ListByOwnerQuery, ListChannelsQuery};
|
||||
pub use deps::ChannelCommandDeps;
|
||||
|
||||
use domain::models::Channel;
|
||||
use domain::value_objects::{ChannelId, UserId};
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
use domain::value_objects::{ChannelId, UserId};
|
||||
|
||||
pub struct GetChannelQuery {
|
||||
pub channel_id: ChannelId,
|
||||
}
|
||||
|
||||
pub struct ListChannelsQuery;
|
||||
|
||||
pub struct ListByOwnerQuery {
|
||||
pub owner_id: UserId,
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::testing::{InMemoryChannelRepository, NoopEventPublisher};
|
||||
use domain::value_objects::{ChannelId, UserId};
|
||||
|
||||
use crate::channels::commands::CreateChannelCommand;
|
||||
use crate::channels::deps::{ChannelCommandDeps, ChannelQueryDeps};
|
||||
use crate::channels::queries::GetChannelQuery;
|
||||
use crate::channels::{create, get};
|
||||
|
||||
fn make_deps() -> (ChannelCommandDeps, ChannelQueryDeps) {
|
||||
let repo = Arc::new(InMemoryChannelRepository::new());
|
||||
let cmd_deps = ChannelCommandDeps {
|
||||
channel_command: repo.clone(),
|
||||
channel_query: repo.clone(),
|
||||
event_publisher: Arc::new(NoopEventPublisher::new()),
|
||||
};
|
||||
let query_deps = ChannelQueryDeps {
|
||||
channel_query: repo,
|
||||
};
|
||||
(cmd_deps, query_deps)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_existing_channel() {
|
||||
let (cmd_deps, query_deps) = make_deps();
|
||||
|
||||
let channel = create::execute(
|
||||
&cmd_deps,
|
||||
CreateChannelCommand {
|
||||
owner_id: UserId::generate(),
|
||||
name: "Findable".into(),
|
||||
timezone: "UTC".into(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let found = get::execute(
|
||||
&query_deps,
|
||||
GetChannelQuery {
|
||||
channel_id: channel.id(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(found.is_some());
|
||||
assert_eq!(found.unwrap().name(), "Findable");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_nonexistent_returns_none() {
|
||||
let (_, query_deps) = make_deps();
|
||||
|
||||
let found = get::execute(
|
||||
&query_deps,
|
||||
GetChannelQuery {
|
||||
channel_id: ChannelId::generate(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(found.is_none());
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::testing::{InMemoryChannelRepository, NoopEventPublisher};
|
||||
use domain::value_objects::UserId;
|
||||
|
||||
use crate::channels::commands::CreateChannelCommand;
|
||||
use crate::channels::deps::{ChannelCommandDeps, ChannelQueryDeps};
|
||||
use crate::channels::queries::ListChannelsQuery;
|
||||
use crate::channels::{create, list};
|
||||
|
||||
fn make_deps() -> (ChannelCommandDeps, ChannelQueryDeps) {
|
||||
let repo = Arc::new(InMemoryChannelRepository::new());
|
||||
let cmd_deps = ChannelCommandDeps {
|
||||
channel_command: repo.clone(),
|
||||
channel_query: repo.clone(),
|
||||
event_publisher: Arc::new(NoopEventPublisher::new()),
|
||||
};
|
||||
let query_deps = ChannelQueryDeps {
|
||||
channel_query: repo,
|
||||
};
|
||||
(cmd_deps, query_deps)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_empty_returns_empty() {
|
||||
let (_, query_deps) = make_deps();
|
||||
|
||||
let channels = list::execute(&query_deps, ListChannelsQuery).await.unwrap();
|
||||
assert!(channels.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_returns_all_channels() {
|
||||
let (cmd_deps, query_deps) = make_deps();
|
||||
|
||||
for name in ["A", "B", "C"] {
|
||||
create::execute(
|
||||
&cmd_deps,
|
||||
CreateChannelCommand {
|
||||
owner_id: UserId::generate(),
|
||||
name: name.into(),
|
||||
timezone: "UTC".into(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let channels = list::execute(&query_deps, ListChannelsQuery).await.unwrap();
|
||||
assert_eq!(channels.len(), 3);
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::testing::{InMemoryChannelRepository, NoopEventPublisher};
|
||||
use domain::value_objects::UserId;
|
||||
|
||||
use crate::channels::commands::CreateChannelCommand;
|
||||
use crate::channels::deps::{ChannelCommandDeps, ChannelQueryDeps};
|
||||
use crate::channels::queries::ListByOwnerQuery;
|
||||
use crate::channels::{create, list_by_owner};
|
||||
|
||||
fn make_deps() -> (ChannelCommandDeps, ChannelQueryDeps) {
|
||||
let repo = Arc::new(InMemoryChannelRepository::new());
|
||||
let cmd_deps = ChannelCommandDeps {
|
||||
channel_command: repo.clone(),
|
||||
channel_query: repo.clone(),
|
||||
event_publisher: Arc::new(NoopEventPublisher::new()),
|
||||
};
|
||||
let query_deps = ChannelQueryDeps {
|
||||
channel_query: repo,
|
||||
};
|
||||
(cmd_deps, query_deps)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn filters_by_owner() {
|
||||
let (cmd_deps, query_deps) = make_deps();
|
||||
let alice = UserId::generate();
|
||||
let bob = UserId::generate();
|
||||
|
||||
// Alice: 2 channels
|
||||
for name in ["Alice-1", "Alice-2"] {
|
||||
create::execute(
|
||||
&cmd_deps,
|
||||
CreateChannelCommand {
|
||||
owner_id: alice,
|
||||
name: name.into(),
|
||||
timezone: "UTC".into(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Bob: 1 channel
|
||||
create::execute(
|
||||
&cmd_deps,
|
||||
CreateChannelCommand {
|
||||
owner_id: bob,
|
||||
name: "Bob-1".into(),
|
||||
timezone: "UTC".into(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let alice_channels = list_by_owner::execute(
|
||||
&query_deps,
|
||||
ListByOwnerQuery {
|
||||
owner_id: alice,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(alice_channels.len(), 2);
|
||||
assert!(alice_channels.iter().all(|c| c.owner_id() == alice));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn no_channels_returns_empty() {
|
||||
let (_, query_deps) = make_deps();
|
||||
|
||||
let channels = list_by_owner::execute(
|
||||
&query_deps,
|
||||
ListByOwnerQuery {
|
||||
owner_id: UserId::generate(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(channels.is_empty());
|
||||
}
|
||||
@@ -5,12 +5,6 @@ pub struct SaveSnapshotCommand {
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
pub struct PatchLabelCommand {
|
||||
pub channel_id: ChannelId,
|
||||
pub snapshot_id: SnapshotId,
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
pub struct RestoreSnapshotCommand {
|
||||
pub channel_id: ChannelId,
|
||||
pub snapshot_id: SnapshotId,
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
use domain::models::ChannelConfigSnapshot;
|
||||
use domain::DomainResult;
|
||||
|
||||
use super::deps::ConfigSnapshotDeps;
|
||||
use super::queries::GetSnapshotQuery;
|
||||
|
||||
pub async fn execute(
|
||||
deps: &ConfigSnapshotDeps,
|
||||
query: GetSnapshotQuery,
|
||||
) -> DomainResult<Option<ChannelConfigSnapshot>> {
|
||||
deps.channel_query
|
||||
.get_config_snapshot(query.channel_id, query.snapshot_id)
|
||||
.await
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
use domain::models::ChannelConfigSnapshot;
|
||||
use domain::DomainResult;
|
||||
|
||||
use super::deps::ConfigSnapshotDeps;
|
||||
use super::queries::ListSnapshotsQuery;
|
||||
|
||||
pub async fn execute(
|
||||
deps: &ConfigSnapshotDeps,
|
||||
query: ListSnapshotsQuery,
|
||||
) -> DomainResult<Vec<ChannelConfigSnapshot>> {
|
||||
deps.channel_query.list_config_snapshots(query.channel_id).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/list.rs"]
|
||||
mod tests;
|
||||
@@ -1,12 +1,7 @@
|
||||
pub mod commands;
|
||||
pub mod deps;
|
||||
pub mod get;
|
||||
pub mod list;
|
||||
pub mod patch_label;
|
||||
pub mod queries;
|
||||
pub mod restore;
|
||||
pub mod save;
|
||||
|
||||
pub use commands::{PatchLabelCommand, RestoreSnapshotCommand, SaveSnapshotCommand};
|
||||
pub use commands::{RestoreSnapshotCommand, SaveSnapshotCommand};
|
||||
pub use deps::ConfigSnapshotDeps;
|
||||
pub use queries::{GetSnapshotQuery, ListSnapshotsQuery};
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
use domain::models::ChannelConfigSnapshot;
|
||||
use domain::DomainResult;
|
||||
|
||||
use super::commands::PatchLabelCommand;
|
||||
use super::deps::ConfigSnapshotDeps;
|
||||
|
||||
pub async fn execute(
|
||||
deps: &ConfigSnapshotDeps,
|
||||
cmd: PatchLabelCommand,
|
||||
) -> DomainResult<Option<ChannelConfigSnapshot>> {
|
||||
deps.channel_command
|
||||
.patch_config_snapshot_label(cmd.channel_id, cmd.snapshot_id, cmd.label)
|
||||
.await
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
use domain::value_objects::{ChannelId, SnapshotId};
|
||||
|
||||
pub struct ListSnapshotsQuery {
|
||||
pub channel_id: ChannelId,
|
||||
}
|
||||
|
||||
pub struct GetSnapshotQuery {
|
||||
pub channel_id: ChannelId,
|
||||
pub snapshot_id: SnapshotId,
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::models::Channel;
|
||||
use domain::testing::InMemoryChannelRepository;
|
||||
use domain::value_objects::UserId;
|
||||
|
||||
use crate::config_snapshots::commands::SaveSnapshotCommand;
|
||||
use crate::config_snapshots::deps::ConfigSnapshotDeps;
|
||||
use crate::config_snapshots::queries::ListSnapshotsQuery;
|
||||
use crate::config_snapshots::{list, save};
|
||||
|
||||
fn make_deps() -> (ConfigSnapshotDeps, Arc<InMemoryChannelRepository>) {
|
||||
let repo = Arc::new(InMemoryChannelRepository::new());
|
||||
let deps = ConfigSnapshotDeps {
|
||||
channel_command: repo.clone(),
|
||||
channel_query: repo.clone(),
|
||||
};
|
||||
(deps, repo)
|
||||
}
|
||||
|
||||
async fn seed_channel(repo: &InMemoryChannelRepository) -> Channel {
|
||||
let channel = Channel::new(UserId::generate(), "Test Channel", "UTC");
|
||||
repo.channels
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(channel.id(), channel.clone());
|
||||
channel
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_empty() {
|
||||
let (deps, repo) = make_deps();
|
||||
let channel = seed_channel(&repo).await;
|
||||
|
||||
let snaps = list::execute(
|
||||
&deps,
|
||||
ListSnapshotsQuery {
|
||||
channel_id: channel.id(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(snaps.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_returns_saved_snapshots() {
|
||||
let (deps, repo) = make_deps();
|
||||
let channel = seed_channel(&repo).await;
|
||||
|
||||
for label in ["first", "second"] {
|
||||
save::execute(
|
||||
&deps,
|
||||
SaveSnapshotCommand {
|
||||
channel_id: channel.id(),
|
||||
label: Some(label.into()),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let snaps = list::execute(
|
||||
&deps,
|
||||
ListSnapshotsQuery {
|
||||
channel_id: channel.id(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(snaps.len(), 2);
|
||||
// Newest first
|
||||
assert_eq!(snaps[0].version_num(), 2);
|
||||
assert_eq!(snaps[1].version_num(), 1);
|
||||
}
|
||||
@@ -9,7 +9,3 @@ pub struct LibraryCommandDeps {
|
||||
pub provider_registry: Arc<dyn IProviderRegistry>,
|
||||
pub event_publisher: Arc<dyn EventPublisher>,
|
||||
}
|
||||
|
||||
pub struct LibraryQueryDeps {
|
||||
pub library_query: Arc<dyn LibraryQuery>,
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
use domain::models::MediaItem;
|
||||
use domain::DomainResult;
|
||||
|
||||
use super::deps::LibraryQueryDeps;
|
||||
use super::queries::GetItemQuery;
|
||||
|
||||
pub async fn execute(
|
||||
deps: &LibraryQueryDeps,
|
||||
query: GetItemQuery,
|
||||
) -> DomainResult<Option<MediaItem>> {
|
||||
deps.library_query.get_by_id(&query.item_id).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/get_item.rs"]
|
||||
mod tests;
|
||||
@@ -1,16 +0,0 @@
|
||||
use domain::models::LibrarySyncLogEntry;
|
||||
use domain::DomainResult;
|
||||
|
||||
use super::deps::LibraryQueryDeps;
|
||||
use super::queries::GetSyncStatusQuery;
|
||||
|
||||
pub async fn execute(
|
||||
deps: &LibraryQueryDeps,
|
||||
_query: GetSyncStatusQuery,
|
||||
) -> DomainResult<Vec<LibrarySyncLogEntry>> {
|
||||
deps.library_query.latest_sync_status().await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/get_sync_status.rs"]
|
||||
mod tests;
|
||||
@@ -1,18 +0,0 @@
|
||||
use domain::models::LibraryCollection;
|
||||
use domain::DomainResult;
|
||||
|
||||
use super::deps::LibraryQueryDeps;
|
||||
use super::queries::ListCollectionsQuery;
|
||||
|
||||
pub async fn execute(
|
||||
deps: &LibraryQueryDeps,
|
||||
query: ListCollectionsQuery,
|
||||
) -> DomainResult<Vec<LibraryCollection>> {
|
||||
deps.library_query
|
||||
.list_collections(query.provider_id.as_deref())
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/list_collections.rs"]
|
||||
mod tests;
|
||||
@@ -1,21 +0,0 @@
|
||||
use domain::DomainResult;
|
||||
|
||||
use super::deps::LibraryQueryDeps;
|
||||
use super::parse_content_type;
|
||||
use super::queries::ListGenresQuery;
|
||||
|
||||
pub async fn execute(deps: &LibraryQueryDeps, query: ListGenresQuery) -> DomainResult<Vec<String>> {
|
||||
let content_type = query
|
||||
.content_type
|
||||
.as_deref()
|
||||
.map(parse_content_type)
|
||||
.transpose()?;
|
||||
|
||||
deps.library_query
|
||||
.list_genres(content_type.as_ref(), query.provider_id.as_deref())
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/list_genres.rs"]
|
||||
mod tests;
|
||||
@@ -1,18 +0,0 @@
|
||||
use domain::models::SeasonSummary;
|
||||
use domain::DomainResult;
|
||||
|
||||
use super::deps::LibraryQueryDeps;
|
||||
use super::queries::ListSeasonsQuery;
|
||||
|
||||
pub async fn execute(
|
||||
deps: &LibraryQueryDeps,
|
||||
query: ListSeasonsQuery,
|
||||
) -> DomainResult<Vec<SeasonSummary>> {
|
||||
deps.library_query
|
||||
.list_seasons(&query.series_name, query.provider_id.as_deref())
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/list_seasons.rs"]
|
||||
mod tests;
|
||||
@@ -1,22 +0,0 @@
|
||||
use domain::models::ShowSummary;
|
||||
use domain::DomainResult;
|
||||
|
||||
use super::deps::LibraryQueryDeps;
|
||||
use super::queries::ListShowsQuery;
|
||||
|
||||
pub async fn execute(
|
||||
deps: &LibraryQueryDeps,
|
||||
query: ListShowsQuery,
|
||||
) -> DomainResult<Vec<ShowSummary>> {
|
||||
deps.library_query
|
||||
.list_shows(
|
||||
query.provider_id.as_deref(),
|
||||
query.search_term.as_deref(),
|
||||
&query.genres,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/list_shows.rs"]
|
||||
mod tests;
|
||||
@@ -1,26 +1,17 @@
|
||||
pub mod commands;
|
||||
pub mod deps;
|
||||
pub mod get_item;
|
||||
pub mod get_sync_status;
|
||||
pub mod list_collections;
|
||||
pub mod list_genres;
|
||||
pub mod list_seasons;
|
||||
pub mod list_shows;
|
||||
pub mod queries;
|
||||
pub mod search;
|
||||
pub mod sync;
|
||||
|
||||
pub use commands::TriggerSyncCommand;
|
||||
pub use deps::{LibraryCommandDeps, LibraryQueryDeps};
|
||||
pub use queries::{
|
||||
GetItemQuery, GetSyncStatusQuery, ListCollectionsQuery, ListGenresQuery, ListSeasonsQuery,
|
||||
ListShowsQuery, SearchItemsQuery,
|
||||
};
|
||||
pub use deps::LibraryCommandDeps;
|
||||
pub use queries::SearchItemsQuery;
|
||||
|
||||
use domain::errors::{DomainError, DomainResult};
|
||||
use domain::value_objects::ContentType;
|
||||
|
||||
pub(crate) fn parse_content_type(s: &str) -> DomainResult<ContentType> {
|
||||
pub fn parse_content_type(s: &str) -> DomainResult<ContentType> {
|
||||
match s {
|
||||
"movie" => Ok(ContentType::Movie),
|
||||
"episode" => Ok(ContentType::Episode),
|
||||
|
||||
@@ -10,29 +10,3 @@ pub struct SearchItemsQuery {
|
||||
pub offset: u32,
|
||||
pub limit: u32,
|
||||
}
|
||||
|
||||
pub struct ListCollectionsQuery {
|
||||
pub provider_id: Option<String>,
|
||||
}
|
||||
|
||||
pub struct ListShowsQuery {
|
||||
pub provider_id: Option<String>,
|
||||
pub search_term: Option<String>,
|
||||
pub genres: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct ListSeasonsQuery {
|
||||
pub series_name: String,
|
||||
pub provider_id: Option<String>,
|
||||
}
|
||||
|
||||
pub struct ListGenresQuery {
|
||||
pub content_type: Option<String>,
|
||||
pub provider_id: Option<String>,
|
||||
}
|
||||
|
||||
pub struct GetItemQuery {
|
||||
pub item_id: String,
|
||||
}
|
||||
|
||||
pub struct GetSyncStatusQuery;
|
||||
|
||||
@@ -2,12 +2,12 @@ use domain::DomainResult;
|
||||
use domain::models::MediaItem;
|
||||
use domain::value_objects::LibrarySearchFilter;
|
||||
|
||||
use super::deps::LibraryQueryDeps;
|
||||
use super::deps::LibraryCommandDeps;
|
||||
use super::parse_content_type;
|
||||
use super::queries::SearchItemsQuery;
|
||||
|
||||
pub async fn execute(
|
||||
deps: &LibraryQueryDeps,
|
||||
deps: &LibraryCommandDeps,
|
||||
query: SearchItemsQuery,
|
||||
) -> DomainResult<(Vec<MediaItem>, u32)> {
|
||||
let content_type = query
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
use domain::models::MediaItem;
|
||||
use domain::value_objects::ContentType;
|
||||
|
||||
use crate::library::get_item;
|
||||
use crate::library::queries::GetItemQuery;
|
||||
|
||||
#[path = "helpers.rs"]
|
||||
mod helpers;
|
||||
|
||||
fn seed_item(repo: &std::sync::Arc<domain::testing::InMemoryLibraryRepository>) {
|
||||
let item = MediaItem::new_library("test", "m1", "Die Hard", ContentType::Movie, 7800, "2026-01-01");
|
||||
repo.items
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(item.id().value().to_string(), item);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_existing_item() {
|
||||
let (_cmd, query, repo) = helpers::make_deps();
|
||||
seed_item(&repo);
|
||||
|
||||
let item = get_item::execute(
|
||||
&query,
|
||||
GetItemQuery {
|
||||
item_id: "test::m1".into(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(item.is_some());
|
||||
assert_eq!(item.unwrap().title(), "Die Hard");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_nonexistent_item_returns_none() {
|
||||
let (_cmd, query, _repo) = helpers::make_deps();
|
||||
|
||||
let item = get_item::execute(
|
||||
&query,
|
||||
GetItemQuery {
|
||||
item_id: "test::missing".into(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(item.is_none());
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
use crate::library::get_sync_status;
|
||||
use crate::library::queries::GetSyncStatusQuery;
|
||||
|
||||
#[path = "helpers.rs"]
|
||||
mod helpers;
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_sync_status_empty() {
|
||||
let (_cmd, query, _repo) = helpers::make_deps();
|
||||
|
||||
let entries = get_sync_status::execute(&query, GetSyncStatusQuery)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(entries.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_sync_status_after_sync() {
|
||||
let (cmd, query, _repo) = helpers::make_deps();
|
||||
|
||||
// Start a sync log entry
|
||||
let log_id = domain::ports::LibraryCommand::log_sync_start(&*cmd.library_command, "test")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Finish it
|
||||
let result = domain::models::LibrarySyncResult::new("test", 42, 500);
|
||||
domain::ports::LibraryCommand::log_sync_finish(&*cmd.library_command, log_id, &result)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let entries = get_sync_status::execute(&query, GetSyncStatusQuery)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(entries[0].provider_id(), "test");
|
||||
assert_eq!(entries[0].items_found(), 42);
|
||||
assert_eq!(entries[0].status(), "success");
|
||||
}
|
||||
@@ -11,9 +11,8 @@ use domain::ports::{
|
||||
use domain::testing::{InMemoryLibraryRepository, NoopEventPublisher, NoopLibrarySync};
|
||||
use domain::value_objects::{ContentType, MediaFilter, MediaItemId};
|
||||
|
||||
use crate::library::deps::{LibraryCommandDeps, LibraryQueryDeps};
|
||||
use crate::library::deps::LibraryCommandDeps;
|
||||
|
||||
/// Minimal IProviderRegistry for library tests.
|
||||
pub(crate) struct TestProviderRegistry;
|
||||
|
||||
#[async_trait]
|
||||
@@ -83,24 +82,14 @@ impl IProviderRegistry for TestProviderRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build LibraryCommandDeps and LibraryQueryDeps backed by InMemory repos.
|
||||
///
|
||||
/// Returns deps plus the underlying repo for seeding test data.
|
||||
pub(crate) fn make_deps() -> (
|
||||
LibraryCommandDeps,
|
||||
LibraryQueryDeps,
|
||||
Arc<InMemoryLibraryRepository>,
|
||||
) {
|
||||
pub(crate) fn make_deps() -> (LibraryCommandDeps, Arc<InMemoryLibraryRepository>) {
|
||||
let repo = Arc::new(InMemoryLibraryRepository::new());
|
||||
let cmd_deps = LibraryCommandDeps {
|
||||
let deps = LibraryCommandDeps {
|
||||
library_command: repo.clone(),
|
||||
library_query: repo.clone(),
|
||||
library_sync: Arc::new(NoopLibrarySync::new()),
|
||||
provider_registry: Arc::new(TestProviderRegistry),
|
||||
event_publisher: Arc::new(NoopEventPublisher::new()),
|
||||
};
|
||||
let query_deps = LibraryQueryDeps {
|
||||
library_query: repo.clone(),
|
||||
};
|
||||
(cmd_deps, query_deps, repo)
|
||||
(deps, repo)
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
use domain::models::{MediaItem, MediaItemRow};
|
||||
use domain::value_objects::{ContentType, MediaItemId, MediaRole};
|
||||
|
||||
use crate::library::list_collections;
|
||||
use crate::library::queries::ListCollectionsQuery;
|
||||
|
||||
#[path = "helpers.rs"]
|
||||
mod helpers;
|
||||
|
||||
fn seed_with_collections(repo: &std::sync::Arc<domain::testing::InMemoryLibraryRepository>) {
|
||||
let mut store = repo.items.lock().unwrap();
|
||||
|
||||
let item = MediaItem::from_persistence(MediaItemRow {
|
||||
id: MediaItemId::new("test::m1"),
|
||||
provider_id: "test".into(),
|
||||
external_id: "m1".into(),
|
||||
title: "Die Hard".into(),
|
||||
content_type: ContentType::Movie,
|
||||
duration_secs: 7800,
|
||||
description: None,
|
||||
series_name: None,
|
||||
season_number: None,
|
||||
episode_number: None,
|
||||
year: None,
|
||||
genres: vec![],
|
||||
tags: vec![],
|
||||
collection_id: Some("col-1".into()),
|
||||
collection_name: Some("Movies".into()),
|
||||
collection_type: Some("movies".into()),
|
||||
thumbnail_url: None,
|
||||
synced_at: Some("2026-01-01".into()),
|
||||
role: MediaRole::default(),
|
||||
});
|
||||
store.insert(item.id().value().to_string(), item);
|
||||
|
||||
let item2 = MediaItem::from_persistence(MediaItemRow {
|
||||
id: MediaItemId::new("test::e1"),
|
||||
provider_id: "test".into(),
|
||||
external_id: "e1".into(),
|
||||
title: "BB S01E01".into(),
|
||||
content_type: ContentType::Episode,
|
||||
duration_secs: 2700,
|
||||
description: None,
|
||||
series_name: Some("Breaking Bad".into()),
|
||||
season_number: Some(1),
|
||||
episode_number: Some(1),
|
||||
year: None,
|
||||
genres: vec![],
|
||||
tags: vec![],
|
||||
collection_id: Some("col-2".into()),
|
||||
collection_name: Some("TV Shows".into()),
|
||||
collection_type: Some("tvshows".into()),
|
||||
thumbnail_url: None,
|
||||
synced_at: Some("2026-01-01".into()),
|
||||
role: MediaRole::default(),
|
||||
});
|
||||
store.insert(item2.id().value().to_string(), item2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_collections_returns_distinct() {
|
||||
let (_cmd, query, repo) = helpers::make_deps();
|
||||
seed_with_collections(&repo);
|
||||
|
||||
let cols = list_collections::execute(
|
||||
&query,
|
||||
ListCollectionsQuery { provider_id: None },
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(cols.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_collections_empty_library() {
|
||||
let (_cmd, query, _repo) = helpers::make_deps();
|
||||
|
||||
let cols = list_collections::execute(
|
||||
&query,
|
||||
ListCollectionsQuery { provider_id: None },
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(cols.is_empty());
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
use domain::models::{MediaItem, MediaItemRow};
|
||||
use domain::value_objects::{ContentType, MediaItemId, MediaRole};
|
||||
|
||||
use crate::library::list_genres;
|
||||
use crate::library::queries::ListGenresQuery;
|
||||
|
||||
#[path = "helpers.rs"]
|
||||
mod helpers;
|
||||
|
||||
fn seed_with_genres(repo: &std::sync::Arc<domain::testing::InMemoryLibraryRepository>) {
|
||||
let mut store = repo.items.lock().unwrap();
|
||||
|
||||
let item1 = MediaItem::from_persistence(MediaItemRow {
|
||||
id: MediaItemId::new("test::m1"),
|
||||
provider_id: "test".into(),
|
||||
external_id: "m1".into(),
|
||||
title: "Die Hard".into(),
|
||||
content_type: ContentType::Movie,
|
||||
duration_secs: 7800,
|
||||
description: None,
|
||||
series_name: None,
|
||||
season_number: None,
|
||||
episode_number: None,
|
||||
year: None,
|
||||
genres: vec!["Action".into(), "Thriller".into()],
|
||||
tags: vec![],
|
||||
collection_id: None,
|
||||
collection_name: None,
|
||||
collection_type: None,
|
||||
thumbnail_url: None,
|
||||
synced_at: Some("2026-01-01".into()),
|
||||
role: MediaRole::default(),
|
||||
});
|
||||
let item2 = MediaItem::from_persistence(MediaItemRow {
|
||||
id: MediaItemId::new("test::m2"),
|
||||
provider_id: "test".into(),
|
||||
external_id: "m2".into(),
|
||||
title: "Alien".into(),
|
||||
content_type: ContentType::Movie,
|
||||
duration_secs: 7020,
|
||||
description: None,
|
||||
series_name: None,
|
||||
season_number: None,
|
||||
episode_number: None,
|
||||
year: None,
|
||||
genres: vec!["Sci-Fi".into(), "Action".into()],
|
||||
tags: vec![],
|
||||
collection_id: None,
|
||||
collection_name: None,
|
||||
collection_type: None,
|
||||
thumbnail_url: None,
|
||||
synced_at: Some("2026-01-01".into()),
|
||||
role: MediaRole::default(),
|
||||
});
|
||||
|
||||
store.insert(item1.id().value().to_string(), item1);
|
||||
store.insert(item2.id().value().to_string(), item2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_genres_returns_unique() {
|
||||
let (_cmd, query, repo) = helpers::make_deps();
|
||||
seed_with_genres(&repo);
|
||||
|
||||
let genres = list_genres::execute(
|
||||
&query,
|
||||
ListGenresQuery {
|
||||
content_type: None,
|
||||
provider_id: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(genres.len(), 3); // Action, Sci-Fi, Thriller (deduped)
|
||||
assert!(genres.contains(&"Action".to_string()));
|
||||
assert!(genres.contains(&"Sci-Fi".to_string()));
|
||||
assert!(genres.contains(&"Thriller".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_genres_empty_library() {
|
||||
let (_cmd, query, _repo) = helpers::make_deps();
|
||||
|
||||
let genres = list_genres::execute(
|
||||
&query,
|
||||
ListGenresQuery {
|
||||
content_type: None,
|
||||
provider_id: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(genres.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_genres_invalid_content_type_errors() {
|
||||
let (_cmd, query, _repo) = helpers::make_deps();
|
||||
|
||||
let result = list_genres::execute(
|
||||
&query,
|
||||
ListGenresQuery {
|
||||
content_type: Some("invalid".into()),
|
||||
provider_id: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
use domain::models::{MediaItem, MediaItemRow};
|
||||
use domain::value_objects::{ContentType, MediaItemId, MediaRole};
|
||||
|
||||
use crate::library::list_seasons;
|
||||
use crate::library::queries::ListSeasonsQuery;
|
||||
|
||||
#[path = "helpers.rs"]
|
||||
mod helpers;
|
||||
|
||||
fn seed_episodes(repo: &std::sync::Arc<domain::testing::InMemoryLibraryRepository>) {
|
||||
let mut store = repo.items.lock().unwrap();
|
||||
|
||||
for (i, season) in [1u32, 1, 2, 2, 2, 3].iter().enumerate() {
|
||||
let item = MediaItem::from_persistence(MediaItemRow {
|
||||
id: MediaItemId::new(format!("test::e{i}")),
|
||||
provider_id: "test".into(),
|
||||
external_id: format!("e{i}"),
|
||||
title: format!("BB S{season:02}E{:02}", i + 1),
|
||||
content_type: ContentType::Episode,
|
||||
duration_secs: 2700,
|
||||
description: None,
|
||||
series_name: Some("Breaking Bad".into()),
|
||||
season_number: Some(*season),
|
||||
episode_number: Some(i as u32 + 1),
|
||||
year: None,
|
||||
genres: vec![],
|
||||
tags: vec![],
|
||||
collection_id: None,
|
||||
collection_name: None,
|
||||
collection_type: None,
|
||||
thumbnail_url: None,
|
||||
synced_at: Some("2026-01-01".into()),
|
||||
role: MediaRole::default(),
|
||||
});
|
||||
store.insert(item.id().value().to_string(), item);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_seasons_for_series() {
|
||||
let (_cmd, query, repo) = helpers::make_deps();
|
||||
seed_episodes(&repo);
|
||||
|
||||
let seasons = list_seasons::execute(
|
||||
&query,
|
||||
ListSeasonsQuery {
|
||||
series_name: "Breaking Bad".into(),
|
||||
provider_id: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(seasons.len(), 3);
|
||||
// Sorted by season_number
|
||||
assert_eq!(seasons[0].season_number(), 1);
|
||||
assert_eq!(seasons[0].episode_count(), 2);
|
||||
assert_eq!(seasons[1].season_number(), 2);
|
||||
assert_eq!(seasons[1].episode_count(), 3);
|
||||
assert_eq!(seasons[2].season_number(), 3);
|
||||
assert_eq!(seasons[2].episode_count(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_seasons_unknown_series() {
|
||||
let (_cmd, query, _repo) = helpers::make_deps();
|
||||
|
||||
let seasons = list_seasons::execute(
|
||||
&query,
|
||||
ListSeasonsQuery {
|
||||
series_name: "Nonexistent".into(),
|
||||
provider_id: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(seasons.is_empty());
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
use domain::models::{MediaItem, MediaItemRow};
|
||||
use domain::value_objects::{ContentType, MediaItemId, MediaRole};
|
||||
|
||||
use crate::library::list_shows;
|
||||
use crate::library::queries::ListShowsQuery;
|
||||
|
||||
#[path = "helpers.rs"]
|
||||
mod helpers;
|
||||
|
||||
fn seed_episodes(repo: &std::sync::Arc<domain::testing::InMemoryLibraryRepository>) {
|
||||
let mut store = repo.items.lock().unwrap();
|
||||
|
||||
for (i, (series, season)) in [
|
||||
("Breaking Bad", 1u32),
|
||||
("Breaking Bad", 1),
|
||||
("Breaking Bad", 2),
|
||||
("The Wire", 1),
|
||||
("The Wire", 1),
|
||||
]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let item = MediaItem::from_persistence(MediaItemRow {
|
||||
id: MediaItemId::new(format!("test::e{i}")),
|
||||
provider_id: "test".into(),
|
||||
external_id: format!("e{i}"),
|
||||
title: format!("{series} S{season:02}E{i:02}"),
|
||||
content_type: ContentType::Episode,
|
||||
duration_secs: 2700,
|
||||
description: None,
|
||||
series_name: Some(series.to_string()),
|
||||
season_number: Some(*season),
|
||||
episode_number: Some(i as u32 + 1),
|
||||
year: None,
|
||||
genres: vec![],
|
||||
tags: vec![],
|
||||
collection_id: None,
|
||||
collection_name: None,
|
||||
collection_type: None,
|
||||
thumbnail_url: None,
|
||||
synced_at: Some("2026-01-01".into()),
|
||||
role: MediaRole::default(),
|
||||
});
|
||||
store.insert(item.id().value().to_string(), item);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_shows_returns_summaries() {
|
||||
let (_cmd, query, repo) = helpers::make_deps();
|
||||
seed_episodes(&repo);
|
||||
|
||||
let shows = list_shows::execute(
|
||||
&query,
|
||||
ListShowsQuery {
|
||||
provider_id: None,
|
||||
search_term: None,
|
||||
genres: vec![],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(shows.len(), 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_shows_with_search_term() {
|
||||
let (_cmd, query, repo) = helpers::make_deps();
|
||||
seed_episodes(&repo);
|
||||
|
||||
let shows = list_shows::execute(
|
||||
&query,
|
||||
ListShowsQuery {
|
||||
provider_id: None,
|
||||
search_term: Some("breaking".into()),
|
||||
genres: vec![],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(shows.len(), 1);
|
||||
assert_eq!(shows[0].series_name(), "Breaking Bad");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_shows_empty() {
|
||||
let (_cmd, query, _repo) = helpers::make_deps();
|
||||
|
||||
let shows = list_shows::execute(
|
||||
&query,
|
||||
ListShowsQuery {
|
||||
provider_id: None,
|
||||
search_term: None,
|
||||
genres: vec![],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(shows.is_empty());
|
||||
}
|
||||
@@ -93,11 +93,11 @@ fn seed_items_with_genres(repo: &std::sync::Arc<domain::testing::InMemoryLibrary
|
||||
|
||||
#[tokio::test]
|
||||
async fn search_empty_filter_returns_all() {
|
||||
let (_cmd, query, repo) = helpers::make_deps();
|
||||
let (deps, repo) = helpers::make_deps();
|
||||
seed_items(&repo);
|
||||
|
||||
let (items, total) = search::execute(
|
||||
&query,
|
||||
&deps,
|
||||
SearchItemsQuery {
|
||||
provider_id: None,
|
||||
content_type: None,
|
||||
@@ -120,11 +120,11 @@ async fn search_empty_filter_returns_all() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn search_by_content_type() {
|
||||
let (_cmd, query, repo) = helpers::make_deps();
|
||||
let (deps, repo) = helpers::make_deps();
|
||||
seed_items(&repo);
|
||||
|
||||
let (items, total) = search::execute(
|
||||
&query,
|
||||
&deps,
|
||||
SearchItemsQuery {
|
||||
provider_id: None,
|
||||
content_type: Some("movie".into()),
|
||||
@@ -147,11 +147,11 @@ async fn search_by_content_type() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn search_by_genre() {
|
||||
let (_cmd, query, repo) = helpers::make_deps();
|
||||
let (deps, repo) = helpers::make_deps();
|
||||
seed_items_with_genres(&repo);
|
||||
|
||||
let (items, total) = search::execute(
|
||||
&query,
|
||||
&deps,
|
||||
SearchItemsQuery {
|
||||
provider_id: None,
|
||||
content_type: None,
|
||||
@@ -175,11 +175,11 @@ async fn search_by_genre() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn search_by_search_term() {
|
||||
let (_cmd, query, repo) = helpers::make_deps();
|
||||
let (deps, repo) = helpers::make_deps();
|
||||
seed_items(&repo);
|
||||
|
||||
let (items, total) = search::execute(
|
||||
&query,
|
||||
&deps,
|
||||
SearchItemsQuery {
|
||||
provider_id: None,
|
||||
content_type: None,
|
||||
@@ -203,10 +203,10 @@ async fn search_by_search_term() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn search_invalid_content_type_errors() {
|
||||
let (_cmd, query, _repo) = helpers::make_deps();
|
||||
let (deps, _repo) = helpers::make_deps();
|
||||
|
||||
let result = search::execute(
|
||||
&query,
|
||||
&deps,
|
||||
SearchItemsQuery {
|
||||
provider_id: None,
|
||||
content_type: Some("bogus".into()),
|
||||
@@ -227,11 +227,11 @@ async fn search_invalid_content_type_errors() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn search_pagination() {
|
||||
let (_cmd, query, repo) = helpers::make_deps();
|
||||
let (deps, repo) = helpers::make_deps();
|
||||
seed_items(&repo);
|
||||
|
||||
let (items, total) = search::execute(
|
||||
&query,
|
||||
&deps,
|
||||
SearchItemsQuery {
|
||||
provider_id: None,
|
||||
content_type: None,
|
||||
|
||||
@@ -6,10 +6,10 @@ mod helpers;
|
||||
|
||||
#[tokio::test]
|
||||
async fn trigger_sync_returns_provider_ids() {
|
||||
let (cmd, _query, _repo) = helpers::make_deps();
|
||||
let (deps, _repo) = helpers::make_deps();
|
||||
|
||||
let ids = sync::execute(
|
||||
&cmd,
|
||||
&deps,
|
||||
TriggerSyncCommand { provider_id: None },
|
||||
)
|
||||
.await
|
||||
@@ -20,10 +20,10 @@ async fn trigger_sync_returns_provider_ids() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn trigger_sync_specific_provider() {
|
||||
let (cmd, _query, _repo) = helpers::make_deps();
|
||||
let (deps, _repo) = helpers::make_deps();
|
||||
|
||||
let ids = sync::execute(
|
||||
&cmd,
|
||||
&deps,
|
||||
TriggerSyncCommand {
|
||||
provider_id: Some("test".into()),
|
||||
},
|
||||
@@ -36,16 +36,15 @@ async fn trigger_sync_specific_provider() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn trigger_sync_while_running_errors() {
|
||||
let (cmd, _query, repo) = helpers::make_deps();
|
||||
let (deps, repo) = helpers::make_deps();
|
||||
|
||||
// Simulate a running sync by inserting a log entry with "running" status
|
||||
repo.items.lock().unwrap(); // just verify repo is accessible
|
||||
let _log_id = domain::ports::LibraryCommand::log_sync_start(&*cmd.library_command, "test")
|
||||
repo.items.lock().unwrap();
|
||||
let _log_id = domain::ports::LibraryCommand::log_sync_start(&*deps.library_command, "test")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = sync::execute(
|
||||
&cmd,
|
||||
&deps,
|
||||
TriggerSyncCommand { provider_id: None },
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -4,7 +4,3 @@ pub struct UpsertProviderCommand {
|
||||
pub config: serde_json::Value,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
pub struct DeleteProviderCommand {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
use domain::DomainResult;
|
||||
|
||||
use super::commands::DeleteProviderCommand;
|
||||
use super::deps::ProviderDeps;
|
||||
|
||||
pub async fn execute(deps: &ProviderDeps, cmd: DeleteProviderCommand) -> DomainResult<()> {
|
||||
deps.provider_config_command.delete(&cmd.id).await
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
use domain::models::ProviderConfigRow;
|
||||
use domain::DomainResult;
|
||||
|
||||
use super::deps::ProviderDeps;
|
||||
use super::queries::GetProviderQuery;
|
||||
|
||||
pub async fn execute(
|
||||
deps: &ProviderDeps,
|
||||
query: GetProviderQuery,
|
||||
) -> DomainResult<Option<ProviderConfigRow>> {
|
||||
deps.provider_config_query.get_by_id(&query.id).await
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
use domain::models::ProviderConfigRow;
|
||||
use domain::DomainResult;
|
||||
|
||||
use super::deps::ProviderDeps;
|
||||
use super::queries::ListProvidersQuery;
|
||||
|
||||
pub async fn execute(
|
||||
deps: &ProviderDeps,
|
||||
_query: ListProvidersQuery,
|
||||
) -> DomainResult<Vec<ProviderConfigRow>> {
|
||||
deps.provider_config_query.get_all().await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/list.rs"]
|
||||
mod tests;
|
||||
@@ -1,11 +1,6 @@
|
||||
pub mod commands;
|
||||
pub mod delete;
|
||||
pub mod deps;
|
||||
pub mod get;
|
||||
pub mod list;
|
||||
pub mod queries;
|
||||
pub mod upsert;
|
||||
|
||||
pub use commands::{DeleteProviderCommand, UpsertProviderCommand};
|
||||
pub use commands::UpsertProviderCommand;
|
||||
pub use deps::ProviderDeps;
|
||||
pub use queries::{GetProviderQuery, ListProvidersQuery};
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
pub struct ListProvidersQuery;
|
||||
|
||||
pub struct GetProviderQuery {
|
||||
pub id: String,
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::testing::InMemoryProviderConfig;
|
||||
|
||||
use crate::providers::commands::UpsertProviderCommand;
|
||||
use crate::providers::deps::ProviderDeps;
|
||||
use crate::providers::queries::ListProvidersQuery;
|
||||
use crate::providers::{list, upsert};
|
||||
|
||||
fn make_deps() -> ProviderDeps {
|
||||
let repo = Arc::new(InMemoryProviderConfig::new());
|
||||
ProviderDeps {
|
||||
provider_config_command: repo.clone(),
|
||||
provider_config_query: repo,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_empty() {
|
||||
let deps = make_deps();
|
||||
let providers = list::execute(&deps, ListProvidersQuery).await.unwrap();
|
||||
assert!(providers.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_returns_all_providers() {
|
||||
let deps = make_deps();
|
||||
|
||||
for id in ["jf-1", "local-1"] {
|
||||
upsert::execute(
|
||||
&deps,
|
||||
UpsertProviderCommand {
|
||||
id: id.into(),
|
||||
provider_type: "jellyfin".into(),
|
||||
config: serde_json::json!({}),
|
||||
enabled: true,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let providers = list::execute(&deps, ListProvidersQuery).await.unwrap();
|
||||
assert_eq!(providers.len(), 2);
|
||||
}
|
||||
@@ -3,8 +3,3 @@ use uuid::Uuid;
|
||||
pub struct GenerateScheduleCommand {
|
||||
pub channel_id: Uuid,
|
||||
}
|
||||
|
||||
pub struct DeleteSchedulesAfterCommand {
|
||||
pub channel_id: Uuid,
|
||||
pub target_generation: u32,
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
use domain::value_objects::ChannelId;
|
||||
use domain::DomainResult;
|
||||
|
||||
use super::commands::DeleteSchedulesAfterCommand;
|
||||
use super::deps::ScheduleDeps;
|
||||
|
||||
pub async fn execute(
|
||||
deps: &ScheduleDeps,
|
||||
cmd: DeleteSchedulesAfterCommand,
|
||||
) -> DomainResult<()> {
|
||||
let channel_id = ChannelId::from(cmd.channel_id);
|
||||
deps.schedule_command
|
||||
.delete_schedules_after(channel_id, cmd.target_generation)
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/delete_after.rs"]
|
||||
mod tests;
|
||||
@@ -1,20 +0,0 @@
|
||||
use chrono::Utc;
|
||||
|
||||
use domain::models::GeneratedSchedule;
|
||||
use domain::value_objects::ChannelId;
|
||||
use domain::DomainResult;
|
||||
|
||||
use super::deps::ScheduleDeps;
|
||||
use super::queries::GetActiveScheduleQuery;
|
||||
|
||||
pub async fn execute(
|
||||
deps: &ScheduleDeps,
|
||||
query: GetActiveScheduleQuery,
|
||||
) -> DomainResult<Option<GeneratedSchedule>> {
|
||||
let channel_id = ChannelId::from(query.channel_id);
|
||||
deps.schedule_query.find_active(channel_id, Utc::now()).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/get_active.rs"]
|
||||
mod tests;
|
||||
@@ -1,20 +0,0 @@
|
||||
use domain::models::GeneratedSchedule;
|
||||
use domain::value_objects::ChannelId;
|
||||
use domain::DomainResult;
|
||||
|
||||
use super::deps::ScheduleDeps;
|
||||
use super::queries::ListHistoryQuery;
|
||||
|
||||
pub async fn execute(
|
||||
deps: &ScheduleDeps,
|
||||
query: ListHistoryQuery,
|
||||
) -> DomainResult<Vec<GeneratedSchedule>> {
|
||||
let channel_id = ChannelId::from(query.channel_id);
|
||||
deps.schedule_query
|
||||
.list_schedule_history(channel_id)
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/list_history.rs"]
|
||||
mod tests;
|
||||
@@ -1,17 +1,11 @@
|
||||
pub mod commands;
|
||||
pub mod delete_after;
|
||||
pub mod deps;
|
||||
pub mod generate;
|
||||
pub mod get_active;
|
||||
pub mod get_current_broadcast;
|
||||
pub mod get_epg;
|
||||
pub mod get_stream_url;
|
||||
pub mod list_history;
|
||||
pub mod queries;
|
||||
|
||||
pub use commands::{DeleteSchedulesAfterCommand, GenerateScheduleCommand};
|
||||
pub use commands::GenerateScheduleCommand;
|
||||
pub use deps::ScheduleDeps;
|
||||
pub use queries::{
|
||||
GetActiveScheduleQuery, GetCurrentBroadcastQuery, GetEpgQuery, GetStreamUrlQuery,
|
||||
ListHistoryQuery,
|
||||
};
|
||||
pub use queries::{GetCurrentBroadcastQuery, GetEpgQuery, GetStreamUrlQuery};
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct GetActiveScheduleQuery {
|
||||
pub channel_id: Uuid,
|
||||
}
|
||||
|
||||
pub struct GetCurrentBroadcastQuery {
|
||||
pub channel_id: Uuid,
|
||||
}
|
||||
@@ -15,7 +11,3 @@ pub struct GetEpgQuery {
|
||||
pub struct GetStreamUrlQuery {
|
||||
pub channel_id: Uuid,
|
||||
}
|
||||
|
||||
pub struct ListHistoryQuery {
|
||||
pub channel_id: Uuid,
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
use domain::models::{Channel, GeneratedSchedule};
|
||||
use domain::value_objects::UserId;
|
||||
|
||||
use crate::schedule::commands::DeleteSchedulesAfterCommand;
|
||||
use crate::schedule::delete_after;
|
||||
use crate::schedule::queries::ListHistoryQuery;
|
||||
use crate::schedule::list_history;
|
||||
|
||||
#[path = "helpers.rs"]
|
||||
mod helpers;
|
||||
use helpers::make_schedule_deps;
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_after_removes_later_generations() {
|
||||
let (deps, channel_repo, schedule_repo) = make_schedule_deps();
|
||||
|
||||
let channel = Channel::new(UserId::generate(), "Cleanup", "UTC");
|
||||
let channel_id = channel.id();
|
||||
channel_repo
|
||||
.channels
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(channel_id, channel);
|
||||
|
||||
// Manually insert schedules with different generations.
|
||||
let now = chrono::Utc::now();
|
||||
for generation in 1..=3 {
|
||||
let sched = GeneratedSchedule::new(
|
||||
channel_id,
|
||||
now,
|
||||
now + chrono::Duration::hours(24),
|
||||
generation,
|
||||
vec![],
|
||||
);
|
||||
schedule_repo
|
||||
.schedules
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(sched.id(), sched);
|
||||
}
|
||||
|
||||
// Delete generations > 1.
|
||||
delete_after::execute(
|
||||
&deps,
|
||||
DeleteSchedulesAfterCommand {
|
||||
channel_id: channel_id.value(),
|
||||
target_generation: 1,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let remaining = list_history::execute(
|
||||
&deps,
|
||||
ListHistoryQuery {
|
||||
channel_id: channel_id.value(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(remaining.len(), 1);
|
||||
assert_eq!(remaining[0].generation(), 1);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
use crate::schedule::get_active;
|
||||
use crate::schedule::queries::GetActiveScheduleQuery;
|
||||
|
||||
#[path = "helpers.rs"]
|
||||
mod helpers;
|
||||
use helpers::make_schedule_deps;
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_none_when_no_schedule_exists() {
|
||||
let (deps, _, _) = make_schedule_deps();
|
||||
|
||||
let result = get_active::execute(
|
||||
&deps,
|
||||
GetActiveScheduleQuery {
|
||||
channel_id: uuid::Uuid::new_v4(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(result.is_none());
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
use crate::schedule::list_history;
|
||||
use crate::schedule::queries::ListHistoryQuery;
|
||||
|
||||
#[path = "helpers.rs"]
|
||||
mod helpers;
|
||||
use helpers::make_schedule_deps;
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_empty_for_new_channel() {
|
||||
let (deps, _, _) = make_schedule_deps();
|
||||
|
||||
let result = list_history::execute(
|
||||
&deps,
|
||||
ListHistoryQuery {
|
||||
channel_id: uuid::Uuid::new_v4(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -3,11 +3,11 @@ use std::sync::Arc;
|
||||
use application::{
|
||||
admin::AdminDeps,
|
||||
auth::AuthDeps,
|
||||
channels::{ChannelCommandDeps, ChannelQueryDeps},
|
||||
channels::ChannelCommandDeps,
|
||||
config::ConfigDeps,
|
||||
config_snapshots::ConfigSnapshotDeps,
|
||||
iptv::IptvDeps,
|
||||
library::{LibraryCommandDeps, LibraryQueryDeps},
|
||||
library::LibraryCommandDeps,
|
||||
providers::ProviderDeps,
|
||||
schedule::ScheduleDeps,
|
||||
};
|
||||
@@ -73,10 +73,6 @@ pub async fn build_app_state(config: Config) -> anyhow::Result<AppState> {
|
||||
event_publisher: event_publisher.clone(),
|
||||
});
|
||||
|
||||
let channel_query_deps = Arc::new(ChannelQueryDeps {
|
||||
channel_query: wire_output.channel_query.clone(),
|
||||
});
|
||||
|
||||
let config_snapshot_deps = Arc::new(ConfigSnapshotDeps {
|
||||
channel_command: wire_output.channel_command.clone(),
|
||||
channel_query: wire_output.channel_query.clone(),
|
||||
@@ -99,10 +95,6 @@ pub async fn build_app_state(config: Config) -> anyhow::Result<AppState> {
|
||||
event_publisher: event_publisher.clone(),
|
||||
});
|
||||
|
||||
let library_query_deps = Arc::new(LibraryQueryDeps {
|
||||
library_query: wire_output.library_query.clone(),
|
||||
});
|
||||
|
||||
let admin_deps = Arc::new(AdminDeps {
|
||||
settings_repo: wire_output.settings.clone(),
|
||||
activity_query: wire_output.activity_query.clone(),
|
||||
@@ -163,19 +155,23 @@ pub async fn build_app_state(config: Config) -> anyhow::Result<AppState> {
|
||||
Ok(AppState {
|
||||
auth_deps,
|
||||
channel_command_deps,
|
||||
channel_query_deps,
|
||||
config_snapshot_deps,
|
||||
config_deps,
|
||||
schedule_deps,
|
||||
library_command_deps,
|
||||
library_query_deps,
|
||||
admin_deps,
|
||||
iptv_deps,
|
||||
provider_deps,
|
||||
channel_query: wire_output.channel_query.clone(),
|
||||
library_query: wire_output.library_query.clone(),
|
||||
schedule_query: wire_output.schedule_query.clone(),
|
||||
settings_repo: wire_output.settings.clone(),
|
||||
activity_query: wire_output.activity_query.clone(),
|
||||
provider_config_query: wire_output.provider_config_query.clone(),
|
||||
provider_config_command: wire_output.provider_config_command.clone(),
|
||||
#[cfg(feature = "auth-jwt")]
|
||||
jwt_validator,
|
||||
_library_sync: library_sync,
|
||||
_settings_repo: wire_output.settings,
|
||||
_event_bus: event_bus,
|
||||
config: config_arc,
|
||||
_sync_trigger: sync_tx,
|
||||
|
||||
@@ -3,7 +3,7 @@ use axum::extract::{Query, State};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use api_types::{ActivityEventResponse, ActivityLogParams, SettingsResponse};
|
||||
use application::admin::{GetActivityLogQuery, UpdateSettingsCommand};
|
||||
use application::admin::UpdateSettingsCommand;
|
||||
|
||||
use crate::errors::AppError;
|
||||
use crate::extractors::AdminUser;
|
||||
@@ -15,8 +15,7 @@ pub async fn get_settings(
|
||||
State(state): State<AppState>,
|
||||
AdminUser(_user): AdminUser,
|
||||
) -> Result<Json<SettingsResponse>, AppError> {
|
||||
let pairs =
|
||||
application::admin::get_settings::execute(&state.admin_deps, application::admin::GetSettingsQuery).await?;
|
||||
let pairs = state.settings_repo.get_all().await?;
|
||||
let settings: HashMap<String, String> = pairs.into_iter().collect();
|
||||
Ok(Json(SettingsResponse { settings }))
|
||||
}
|
||||
@@ -40,10 +39,8 @@ pub async fn get_activity_log(
|
||||
AdminUser(_user): AdminUser,
|
||||
Query(params): Query<ActivityLogParams>,
|
||||
) -> Result<Json<Vec<ActivityEventResponse>>, AppError> {
|
||||
let query = GetActivityLogQuery {
|
||||
limit: params.limit.unwrap_or(DEFAULT_ACTIVITY_LIMIT),
|
||||
};
|
||||
let events = application::admin::activity_log::execute(&state.admin_deps, query).await?;
|
||||
let limit = params.limit.unwrap_or(DEFAULT_ACTIVITY_LIMIT);
|
||||
let events = state.activity_query.recent(limit).await?;
|
||||
Ok(Json(
|
||||
events
|
||||
.into_iter()
|
||||
|
||||
@@ -5,14 +5,8 @@ use api_types::{
|
||||
ChannelResponse, ConfigSnapshotResponse, CreateChannelRequest, PatchSnapshotRequest,
|
||||
UpdateChannelRequest,
|
||||
};
|
||||
use application::channels::{
|
||||
CreateChannelCommand, DeleteChannelCommand, GetChannelQuery, ListByOwnerQuery,
|
||||
ListChannelsQuery, UpdateChannelCommand,
|
||||
};
|
||||
use application::config_snapshots::{
|
||||
GetSnapshotQuery, ListSnapshotsQuery, PatchLabelCommand, RestoreSnapshotCommand,
|
||||
SaveSnapshotCommand,
|
||||
};
|
||||
use application::channels::{CreateChannelCommand, DeleteChannelCommand, UpdateChannelCommand};
|
||||
use application::config_snapshots::{RestoreSnapshotCommand, SaveSnapshotCommand};
|
||||
use domain::DomainError;
|
||||
|
||||
use crate::errors::AppError;
|
||||
@@ -23,8 +17,7 @@ pub async fn list_channels(
|
||||
State(state): State<AppState>,
|
||||
CurrentUser(_user): CurrentUser,
|
||||
) -> Result<Json<Vec<ChannelResponse>>, AppError> {
|
||||
let channels =
|
||||
application::channels::list::execute(&state.channel_query_deps, ListChannelsQuery).await?;
|
||||
let channels = state.channel_query.find_all().await?;
|
||||
Ok(Json(channels.into_iter().map(ChannelResponse::from).collect()))
|
||||
}
|
||||
|
||||
@@ -32,11 +25,7 @@ pub async fn list_my_channels(
|
||||
State(state): State<AppState>,
|
||||
CurrentUser(user): CurrentUser,
|
||||
) -> Result<Json<Vec<ChannelResponse>>, AppError> {
|
||||
let query = ListByOwnerQuery {
|
||||
owner_id: user.id(),
|
||||
};
|
||||
let channels =
|
||||
application::channels::list_by_owner::execute(&state.channel_query_deps, query).await?;
|
||||
let channels = state.channel_query.find_by_owner(user.id()).await?;
|
||||
Ok(Json(channels.into_iter().map(ChannelResponse::from).collect()))
|
||||
}
|
||||
|
||||
@@ -59,10 +48,9 @@ pub async fn get_channel(
|
||||
CurrentUser(_user): CurrentUser,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
) -> Result<Json<ChannelResponse>, AppError> {
|
||||
let query = GetChannelQuery {
|
||||
channel_id: id.into(),
|
||||
};
|
||||
let channel = application::channels::get::execute(&state.channel_query_deps, query)
|
||||
let channel = state
|
||||
.channel_query
|
||||
.find_by_id(id.into())
|
||||
.await?
|
||||
.ok_or_else(|| AppError(DomainError::NotFound(format!("Channel {id} not found"))))?;
|
||||
Ok(Json(ChannelResponse::from(channel)))
|
||||
@@ -119,10 +107,7 @@ pub async fn list_snapshots(
|
||||
CurrentUser(_user): CurrentUser,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
) -> Result<Json<Vec<ConfigSnapshotResponse>>, AppError> {
|
||||
let query = ListSnapshotsQuery {
|
||||
channel_id: id.into(),
|
||||
};
|
||||
let snaps = application::config_snapshots::list::execute(&state.config_snapshot_deps, query).await?;
|
||||
let snaps = state.channel_query.list_config_snapshots(id.into()).await?;
|
||||
Ok(Json(snaps.into_iter().map(ConfigSnapshotResponse::from).collect()))
|
||||
}
|
||||
|
||||
@@ -131,11 +116,9 @@ pub async fn get_snapshot(
|
||||
CurrentUser(_user): CurrentUser,
|
||||
Path((id, snapshot_id)): Path<(uuid::Uuid, uuid::Uuid)>,
|
||||
) -> Result<Json<ConfigSnapshotResponse>, AppError> {
|
||||
let query = GetSnapshotQuery {
|
||||
channel_id: id.into(),
|
||||
snapshot_id: snapshot_id.into(),
|
||||
};
|
||||
let snap = application::config_snapshots::get::execute(&state.config_snapshot_deps, query)
|
||||
let snap = state
|
||||
.channel_query
|
||||
.get_config_snapshot(id.into(), snapshot_id.into())
|
||||
.await?
|
||||
.ok_or_else(|| AppError(DomainError::NotFound("Snapshot not found".into())))?;
|
||||
Ok(Json(ConfigSnapshotResponse::from(snap)))
|
||||
@@ -147,13 +130,10 @@ pub async fn patch_snapshot(
|
||||
Path((id, snapshot_id)): Path<(uuid::Uuid, uuid::Uuid)>,
|
||||
Json(req): Json<PatchSnapshotRequest>,
|
||||
) -> Result<Json<ConfigSnapshotResponse>, AppError> {
|
||||
let cmd = PatchLabelCommand {
|
||||
channel_id: id.into(),
|
||||
snapshot_id: snapshot_id.into(),
|
||||
label: req.label,
|
||||
};
|
||||
let snap =
|
||||
application::config_snapshots::patch_label::execute(&state.config_snapshot_deps, cmd)
|
||||
let snap = state
|
||||
.channel_command_deps
|
||||
.channel_command
|
||||
.patch_config_snapshot_label(id.into(), snapshot_id.into(), req.label)
|
||||
.await?
|
||||
.ok_or_else(|| AppError(DomainError::NotFound("Snapshot not found".into())))?;
|
||||
Ok(Json(ConfigSnapshotResponse::from(snap)))
|
||||
|
||||
@@ -5,10 +5,7 @@ use api_types::{
|
||||
CollectionResponse, GenresParams, LibraryItemResponse, LibrarySearchParams, PaginatedResponse,
|
||||
ProviderParam, SeasonResponse, SeasonsParams, ShowResponse, ShowsParams, SyncStatusEntry,
|
||||
};
|
||||
use application::library::{
|
||||
GetItemQuery, GetSyncStatusQuery, ListCollectionsQuery, ListGenresQuery, ListSeasonsQuery,
|
||||
ListShowsQuery, SearchItemsQuery, TriggerSyncCommand,
|
||||
};
|
||||
use application::library::{SearchItemsQuery, TriggerSyncCommand};
|
||||
use domain::DomainError;
|
||||
|
||||
use crate::errors::AppError;
|
||||
@@ -34,7 +31,7 @@ pub async fn search_items(
|
||||
offset: params.offset.unwrap_or(0),
|
||||
limit: params.limit.unwrap_or(DEFAULT_SEARCH_LIMIT),
|
||||
};
|
||||
let (items, total) = application::library::search::execute(&state.library_query_deps, query).await?;
|
||||
let (items, total) = application::library::search::execute(&state.library_command_deps, query).await?;
|
||||
Ok(Json(PaginatedResponse::new(
|
||||
items.into_iter().map(LibraryItemResponse::from).collect(),
|
||||
total as u64,
|
||||
@@ -46,8 +43,9 @@ pub async fn get_item(
|
||||
CurrentUser(_user): CurrentUser,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Json<LibraryItemResponse>, AppError> {
|
||||
let query = GetItemQuery { item_id: id.clone() };
|
||||
let item = application::library::get_item::execute(&state.library_query_deps, query)
|
||||
let item = state
|
||||
.library_query
|
||||
.get_by_id(&id)
|
||||
.await?
|
||||
.ok_or_else(|| AppError(DomainError::NotFound(format!("Library item {id} not found"))))?;
|
||||
Ok(Json(LibraryItemResponse::from(item)))
|
||||
@@ -58,11 +56,10 @@ pub async fn list_collections(
|
||||
CurrentUser(_user): CurrentUser,
|
||||
Query(params): Query<ProviderParam>,
|
||||
) -> Result<Json<Vec<CollectionResponse>>, AppError> {
|
||||
let query = ListCollectionsQuery {
|
||||
provider_id: params.provider,
|
||||
};
|
||||
let collections =
|
||||
application::library::list_collections::execute(&state.library_query_deps, query).await?;
|
||||
let collections = state
|
||||
.library_query
|
||||
.list_collections(params.provider.as_deref())
|
||||
.await?;
|
||||
Ok(Json(
|
||||
collections
|
||||
.into_iter()
|
||||
@@ -76,12 +73,14 @@ pub async fn list_shows(
|
||||
CurrentUser(_user): CurrentUser,
|
||||
Query(params): Query<ShowsParams>,
|
||||
) -> Result<Json<Vec<ShowResponse>>, AppError> {
|
||||
let query = ListShowsQuery {
|
||||
provider_id: params.provider,
|
||||
search_term: params.search_term,
|
||||
genres: params.genres,
|
||||
};
|
||||
let shows = application::library::list_shows::execute(&state.library_query_deps, query).await?;
|
||||
let shows = state
|
||||
.library_query
|
||||
.list_shows(
|
||||
params.provider.as_deref(),
|
||||
params.search_term.as_deref(),
|
||||
¶ms.genres,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(shows.into_iter().map(ShowResponse::from).collect()))
|
||||
}
|
||||
|
||||
@@ -90,12 +89,10 @@ pub async fn list_seasons(
|
||||
CurrentUser(_user): CurrentUser,
|
||||
Query(params): Query<SeasonsParams>,
|
||||
) -> Result<Json<Vec<SeasonResponse>>, AppError> {
|
||||
let query = ListSeasonsQuery {
|
||||
series_name: params.series_name,
|
||||
provider_id: params.provider,
|
||||
};
|
||||
let seasons =
|
||||
application::library::list_seasons::execute(&state.library_query_deps, query).await?;
|
||||
let seasons = state
|
||||
.library_query
|
||||
.list_seasons(¶ms.series_name, params.provider.as_deref())
|
||||
.await?;
|
||||
Ok(Json(
|
||||
seasons.into_iter().map(SeasonResponse::from).collect(),
|
||||
))
|
||||
@@ -106,12 +103,15 @@ pub async fn list_genres(
|
||||
CurrentUser(_user): CurrentUser,
|
||||
Query(params): Query<GenresParams>,
|
||||
) -> Result<Json<Vec<String>>, AppError> {
|
||||
let query = ListGenresQuery {
|
||||
content_type: params.content_type,
|
||||
provider_id: params.provider,
|
||||
};
|
||||
let genres =
|
||||
application::library::list_genres::execute(&state.library_query_deps, query).await?;
|
||||
let content_type = params
|
||||
.content_type
|
||||
.as_deref()
|
||||
.map(application::library::parse_content_type)
|
||||
.transpose()?;
|
||||
let genres = state
|
||||
.library_query
|
||||
.list_genres(content_type.as_ref(), params.provider.as_deref())
|
||||
.await?;
|
||||
Ok(Json(genres))
|
||||
}
|
||||
|
||||
@@ -119,9 +119,7 @@ pub async fn sync_status(
|
||||
State(state): State<AppState>,
|
||||
CurrentUser(_user): CurrentUser,
|
||||
) -> Result<Json<Vec<SyncStatusEntry>>, AppError> {
|
||||
let entries =
|
||||
application::library::get_sync_status::execute(&state.library_query_deps, GetSyncStatusQuery)
|
||||
.await?;
|
||||
let entries = state.library_query.latest_sync_status().await?;
|
||||
Ok(Json(entries.into_iter().map(SyncStatusEntry::from).collect()))
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@ use axum::Json;
|
||||
use axum::extract::{Path, State};
|
||||
|
||||
use api_types::{ProviderConfigRequest, ProviderConfigResponse};
|
||||
use application::providers::{
|
||||
DeleteProviderCommand, GetProviderQuery, ListProvidersQuery, UpsertProviderCommand,
|
||||
};
|
||||
use application::providers::UpsertProviderCommand;
|
||||
use domain::DomainError;
|
||||
|
||||
use crate::errors::AppError;
|
||||
@@ -15,8 +13,7 @@ pub async fn list_providers(
|
||||
State(state): State<AppState>,
|
||||
AdminUser(_user): AdminUser,
|
||||
) -> Result<Json<Vec<ProviderConfigResponse>>, AppError> {
|
||||
let providers =
|
||||
application::providers::list::execute(&state.provider_deps, ListProvidersQuery).await?;
|
||||
let providers = state.provider_config_query.get_all().await?;
|
||||
Ok(Json(
|
||||
providers
|
||||
.into_iter()
|
||||
@@ -30,8 +27,9 @@ pub async fn get_provider(
|
||||
AdminUser(_user): AdminUser,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Json<ProviderConfigResponse>, AppError> {
|
||||
let query = GetProviderQuery { id: id.clone() };
|
||||
let provider = application::providers::get::execute(&state.provider_deps, query)
|
||||
let provider = state
|
||||
.provider_config_query
|
||||
.get_by_id(&id)
|
||||
.await?
|
||||
.ok_or_else(|| AppError(DomainError::NotFound(format!("Provider {id} not found"))))?;
|
||||
Ok(Json(ProviderConfigResponse::from(provider)))
|
||||
@@ -58,7 +56,6 @@ pub async fn delete_provider(
|
||||
AdminUser(_user): AdminUser,
|
||||
Path(id): Path<String>,
|
||||
) -> Result<axum::http::StatusCode, AppError> {
|
||||
let cmd = DeleteProviderCommand { id };
|
||||
application::providers::delete::execute(&state.provider_deps, cmd).await?;
|
||||
state.provider_config_command.delete(&id).await?;
|
||||
Ok(axum::http::StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@ use axum::Json;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use chrono::Utc;
|
||||
|
||||
use api_types::{
|
||||
CurrentBroadcastResponse, ScheduleHistoryEntry, ScheduleResponse, SlotResponse,
|
||||
};
|
||||
use application::schedule::{
|
||||
GenerateScheduleCommand, GetActiveScheduleQuery, GetCurrentBroadcastQuery, GetEpgQuery,
|
||||
GetStreamUrlQuery, ListHistoryQuery,
|
||||
GenerateScheduleCommand, GetCurrentBroadcastQuery, GetEpgQuery, GetStreamUrlQuery,
|
||||
};
|
||||
use domain::value_objects::ChannelId;
|
||||
|
||||
use crate::errors::AppError;
|
||||
use crate::extractors::CurrentUser;
|
||||
@@ -30,8 +31,8 @@ pub async fn get_active_schedule(
|
||||
CurrentUser(_user): CurrentUser,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
) -> Result<axum::response::Response, AppError> {
|
||||
let query = GetActiveScheduleQuery { channel_id: id };
|
||||
match application::schedule::get_active::execute(&state.schedule_deps, query).await? {
|
||||
let channel_id = ChannelId::from(id);
|
||||
match state.schedule_query.find_active(channel_id, Utc::now()).await? {
|
||||
Some(schedule) => Ok(Json(ScheduleResponse::from(schedule)).into_response()),
|
||||
None => Ok(StatusCode::NO_CONTENT.into_response()),
|
||||
}
|
||||
@@ -85,9 +86,8 @@ pub async fn list_schedule_history(
|
||||
CurrentUser(_user): CurrentUser,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
) -> Result<Json<Vec<ScheduleHistoryEntry>>, AppError> {
|
||||
let query = ListHistoryQuery { channel_id: id };
|
||||
let history =
|
||||
application::schedule::list_history::execute(&state.schedule_deps, query).await?;
|
||||
let channel_id = ChannelId::from(id);
|
||||
let history = state.schedule_query.list_schedule_history(channel_id).await?;
|
||||
Ok(Json(
|
||||
history
|
||||
.into_iter()
|
||||
|
||||
@@ -3,11 +3,11 @@ use std::sync::Arc;
|
||||
use application::{
|
||||
admin::AdminDeps,
|
||||
auth::AuthDeps,
|
||||
channels::{ChannelCommandDeps, ChannelQueryDeps},
|
||||
channels::ChannelCommandDeps,
|
||||
config::ConfigDeps,
|
||||
config_snapshots::ConfigSnapshotDeps,
|
||||
iptv::IptvDeps,
|
||||
library::{LibraryCommandDeps, LibraryQueryDeps},
|
||||
library::LibraryCommandDeps,
|
||||
providers::ProviderDeps,
|
||||
schedule::ScheduleDeps,
|
||||
};
|
||||
@@ -16,21 +16,26 @@ use application::{
|
||||
pub struct AppState {
|
||||
pub auth_deps: Arc<AuthDeps>,
|
||||
pub channel_command_deps: Arc<ChannelCommandDeps>,
|
||||
pub channel_query_deps: Arc<ChannelQueryDeps>,
|
||||
pub config_snapshot_deps: Arc<ConfigSnapshotDeps>,
|
||||
pub config_deps: Arc<ConfigDeps>,
|
||||
pub schedule_deps: Arc<ScheduleDeps>,
|
||||
pub library_command_deps: Arc<LibraryCommandDeps>,
|
||||
pub library_query_deps: Arc<LibraryQueryDeps>,
|
||||
pub admin_deps: Arc<AdminDeps>,
|
||||
pub iptv_deps: Arc<IptvDeps>,
|
||||
pub provider_deps: Arc<ProviderDeps>,
|
||||
|
||||
pub channel_query: Arc<dyn domain::ports::ChannelQuery>,
|
||||
pub library_query: Arc<dyn domain::ports::LibraryQuery>,
|
||||
pub schedule_query: Arc<dyn domain::ports::ScheduleQuery>,
|
||||
pub settings_repo: Arc<dyn domain::ports::AppSettingsRepository>,
|
||||
pub activity_query: Arc<dyn domain::ports::ActivityLogQuery>,
|
||||
pub provider_config_query: Arc<dyn domain::ports::ProviderConfigQuery>,
|
||||
pub provider_config_command: Arc<dyn domain::ports::ProviderConfigCommand>,
|
||||
|
||||
#[cfg(feature = "auth-jwt")]
|
||||
pub jwt_validator: Option<Arc<adapter_auth::JwtValidator>>,
|
||||
|
||||
pub _library_sync: Arc<dyn domain::ports::LibrarySyncAdapter>,
|
||||
pub _settings_repo: Arc<dyn domain::ports::AppSettingsRepository>,
|
||||
pub _event_bus: Arc<adapter_event_publisher::ChannelEventBus>,
|
||||
|
||||
pub config: Arc<infra_wiring::Config>,
|
||||
|
||||
Reference in New Issue
Block a user