application: config_snapshots, admin, providers, iptv

This commit is contained in:
2026-07-12 02:14:07 +02:00
parent ebf0614fdf
commit 466d34b5d0
37 changed files with 895 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
use std::sync::Arc;
use domain::ports::{ChannelQuery, ScheduleQuery};
/// Dependencies for IPTV export use cases.
pub struct IptvDeps {
pub channel_query: Arc<dyn ChannelQuery>,
pub schedule_query: Arc<dyn ScheduleQuery>,
}

View File

@@ -0,0 +1,18 @@
use domain::services::iptv::generate_m3u;
use domain::DomainResult;
use super::deps::IptvDeps;
use super::queries::GetM3uQuery;
/// Generate an M3U playlist for all channels.
///
/// Flow: fetch all channels -> delegate to domain::generate_m3u -> return string.
pub async fn execute(deps: &IptvDeps, query: GetM3uQuery) -> DomainResult<String> {
let channels = deps.channel_query.find_all().await?;
let token = query.token.as_deref().unwrap_or("");
Ok(generate_m3u(&channels, &query.base_url, token))
}
#[cfg(test)]
#[path = "tests/m3u.rs"]
mod tests;

View File

@@ -0,0 +1,7 @@
pub mod deps;
pub mod m3u;
pub mod queries;
pub mod xmltv;
pub use deps::IptvDeps;
pub use queries::{GetM3uQuery, GetXmltvQuery};

View File

@@ -0,0 +1,8 @@
/// Generate an M3U playlist for all channels.
pub struct GetM3uQuery {
pub base_url: String,
pub token: Option<String>,
}
/// Generate an XMLTV EPG document for all channels.
pub struct GetXmltvQuery;

View File

@@ -0,0 +1,84 @@
use std::sync::Arc;
use domain::models::Channel;
use domain::testing::{InMemoryChannelRepository, InMemoryScheduleRepository};
use domain::value_objects::UserId;
use crate::iptv::deps::IptvDeps;
use crate::iptv::m3u;
use crate::iptv::queries::GetM3uQuery;
fn make_deps() -> (IptvDeps, Arc<InMemoryChannelRepository>) {
let channel_repo = Arc::new(InMemoryChannelRepository::new());
let schedule_repo = Arc::new(InMemoryScheduleRepository::new());
let deps = IptvDeps {
channel_query: channel_repo.clone(),
schedule_query: schedule_repo,
};
(deps, channel_repo)
}
#[tokio::test]
async fn m3u_empty_channels() {
let (deps, _) = make_deps();
let result = m3u::execute(
&deps,
GetM3uQuery {
base_url: "http://localhost:3000".into(),
token: Some("tok123".into()),
},
)
.await
.unwrap();
assert_eq!(result, "#EXTM3U\n");
}
#[tokio::test]
async fn m3u_includes_channels() {
let (deps, repo) = make_deps();
let ch = Channel::new(UserId::generate(), "Test TV", "UTC");
repo.channels
.lock()
.unwrap()
.insert(ch.id().value(), ch.clone());
let result = m3u::execute(
&deps,
GetM3uQuery {
base_url: "http://localhost:3000".into(),
token: Some("mytoken".into()),
},
)
.await
.unwrap();
assert!(result.starts_with("#EXTM3U\n"));
assert!(result.contains("Test TV"));
assert!(result.contains("token=mytoken"));
}
#[tokio::test]
async fn m3u_no_token() {
let (deps, repo) = make_deps();
let ch = Channel::new(UserId::generate(), "Ch1", "UTC");
repo.channels
.lock()
.unwrap()
.insert(ch.id().value(), ch);
let result = m3u::execute(
&deps,
GetM3uQuery {
base_url: "http://localhost:3000".into(),
token: None,
},
)
.await
.unwrap();
assert!(result.contains("token="));
}

View File

@@ -0,0 +1,27 @@
use std::collections::HashMap;
use chrono::Utc;
use domain::services::iptv::generate_xmltv;
use domain::DomainResult;
use super::deps::IptvDeps;
use super::queries::GetXmltvQuery;
/// Generate an XMLTV EPG document for all channels with active schedules.
///
/// Flow: fetch all channels -> for each, find active schedule -> collect slots
/// -> delegate to domain::generate_xmltv -> return string.
pub async fn execute(deps: &IptvDeps, _query: GetXmltvQuery) -> DomainResult<String> {
let channels = deps.channel_query.find_all().await?;
let now = Utc::now();
let mut slots_by_channel = HashMap::new();
for ch in &channels {
if let Some(schedule) = deps.schedule_query.find_active(ch.id(), now).await? {
slots_by_channel.insert(ch.id(), schedule.slots().to_vec());
}
}
Ok(generate_xmltv(&channels, &slots_by_channel))
}