refactor: replace get_stream_url with get_source_uri per ADR-0002
Providers now expose a source URI (for FFmpeg) instead of a viewer-facing stream URL. Playout Service will own transcoding. - Add SourceUri value object (NetworkUrl | FilePath) - Remove StreamQuality, StreamingProtocol from domain - Simplify ProviderCapabilities (drop streaming_protocol, transcode) - Jellyfin: return static direct stream URL - Local files: return absolute file path - Rename use case get_stream_url -> get_source - Stream endpoint returns JSON SourceUri instead of 307 redirect
This commit is contained in:
@@ -37,20 +37,6 @@ pub(crate) struct JellyfinItem {
|
|||||||
pub recursive_item_count: Option<u32>,
|
pub recursive_item_count: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub(crate) struct JellyfinPlaybackInfoResponse {
|
|
||||||
#[serde(rename = "MediaSources")]
|
|
||||||
pub media_sources: Vec<JellyfinMediaSource>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub(crate) struct JellyfinMediaSource {
|
|
||||||
#[serde(rename = "SupportsDirectStream")]
|
|
||||||
pub supports_direct_stream: bool,
|
|
||||||
#[serde(rename = "DirectStreamUrl")]
|
|
||||||
pub direct_stream_url: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn jellyfin_item_type(ct: &ContentType) -> &'static str {
|
pub(crate) fn jellyfin_item_type(ct: &ContentType) -> &'static str {
|
||||||
match ct {
|
match ct {
|
||||||
ContentType::Movie => "Movie",
|
ContentType::Movie => "Movie",
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
use domain::ports::{
|
use domain::ports::{Collection, IMediaProvider, ProviderCapabilities, SeriesSummary};
|
||||||
Collection, IMediaProvider, ProviderCapabilities, SeriesSummary, StreamQuality,
|
use domain::{ContentType, DomainError, DomainResult, MediaFilter, MediaItem, MediaItemId, SourceUri};
|
||||||
StreamingProtocol,
|
|
||||||
};
|
|
||||||
use domain::{ContentType, DomainError, DomainResult, MediaFilter, MediaItem, MediaItemId};
|
|
||||||
|
|
||||||
use crate::config::JellyfinConfig;
|
use crate::config::JellyfinConfig;
|
||||||
use crate::mapping::{map_jellyfin_item, TICKS_PER_SEC};
|
use crate::mapping::{map_jellyfin_item, TICKS_PER_SEC};
|
||||||
use crate::models::{
|
use crate::models::{jellyfin_item_type, JellyfinItemsResponse};
|
||||||
jellyfin_item_type, JellyfinItemsResponse, JellyfinPlaybackInfoResponse,
|
|
||||||
};
|
|
||||||
|
|
||||||
const FALLBACK_HLS_BITRATE: u32 = 8_000_000;
|
|
||||||
|
|
||||||
pub struct JellyfinMediaProvider {
|
pub struct JellyfinMediaProvider {
|
||||||
client: reqwest::Client,
|
client: reqwest::Client,
|
||||||
@@ -129,16 +122,6 @@ impl JellyfinMediaProvider {
|
|||||||
Ok(items)
|
Ok(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hls_url(&self, item_id: &MediaItemId, bitrate: u32) -> String {
|
|
||||||
format!(
|
|
||||||
"{}/Videos/{}/master.m3u8?videoCodec=h264&audioCodec=aac&VideoBitRate={}&mediaSourceId={}&SubtitleMethod=Hls&subtitleCodec=vtt&api_key={}",
|
|
||||||
self.config.base_url,
|
|
||||||
item_id.as_ref(),
|
|
||||||
bitrate,
|
|
||||||
item_id.as_ref(),
|
|
||||||
self.config.api_key,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -151,9 +134,7 @@ impl IMediaProvider for JellyfinMediaProvider {
|
|||||||
tags: true,
|
tags: true,
|
||||||
decade: true,
|
decade: true,
|
||||||
search: true,
|
search: true,
|
||||||
streaming_protocol: StreamingProtocol::Hls,
|
|
||||||
rescan: false,
|
rescan: false,
|
||||||
transcode: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,52 +331,14 @@ impl IMediaProvider for JellyfinMediaProvider {
|
|||||||
Ok(body.items.into_iter().map(|item| item.name).collect())
|
Ok(body.items.into_iter().map(|item| item.name).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_stream_url(
|
async fn get_source_uri(&self, item_id: &MediaItemId) -> DomainResult<SourceUri> {
|
||||||
&self,
|
Ok(SourceUri::NetworkUrl {
|
||||||
item_id: &MediaItemId,
|
url: format!(
|
||||||
quality: &StreamQuality,
|
"{}/Videos/{}/stream?static=true&api_key={}",
|
||||||
) -> DomainResult<String> {
|
|
||||||
match quality {
|
|
||||||
StreamQuality::Direct => {
|
|
||||||
let url = format!(
|
|
||||||
"{}/Items/{}/PlaybackInfo",
|
|
||||||
self.config.base_url,
|
self.config.base_url,
|
||||||
item_id.as_ref()
|
item_id.as_ref(),
|
||||||
);
|
self.config.api_key,
|
||||||
let resp = self
|
),
|
||||||
.client
|
})
|
||||||
.post(&url)
|
|
||||||
.header("X-Emby-Token", &self.config.api_key)
|
|
||||||
.query(&[
|
|
||||||
("userId", &self.config.user_id),
|
|
||||||
("mediaSourceId", &item_id.as_ref().to_string()),
|
|
||||||
])
|
|
||||||
.json(&serde_json::json!({}))
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
DomainError::InfrastructureError(format!("PlaybackInfo failed: {e}"))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if resp.status().is_success() {
|
|
||||||
let info: JellyfinPlaybackInfoResponse = resp.json().await.map_err(|e| {
|
|
||||||
DomainError::InfrastructureError(format!(
|
|
||||||
"PlaybackInfo parse failed: {e}"
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
if let Some(src) = info.media_sources.first()
|
|
||||||
&& src.supports_direct_stream
|
|
||||||
&& let Some(rel_url) = &src.direct_stream_url
|
|
||||||
{
|
|
||||||
return Ok(format!(
|
|
||||||
"{}{}&api_key={}",
|
|
||||||
self.config.base_url, rel_url, self.config.api_key
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(self.hls_url(item_id, FALLBACK_HLS_BITRATE))
|
|
||||||
}
|
|
||||||
StreamQuality::Transcode(bps) => Ok(self.hls_url(item_id, *bps)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ impl LocalFilesBundle {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let provider =
|
let provider =
|
||||||
LocalFilesProvider::new(Arc::clone(&local_index), &config, transcode_manager.clone());
|
LocalFilesProvider::new(Arc::clone(&local_index), &config);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
provider,
|
provider,
|
||||||
|
|||||||
@@ -1,36 +1,23 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use domain::ports::{
|
use domain::ports::{Collection, IMediaProvider, ProviderCapabilities};
|
||||||
Collection, IMediaProvider, ProviderCapabilities, StreamQuality, StreamingProtocol,
|
use domain::{ContentType, DomainError, DomainResult, MediaFilter, MediaItem, MediaItemId, MediaItemRow, MediaRole, SourceUri};
|
||||||
};
|
|
||||||
use domain::{ContentType, DomainError, DomainResult, MediaFilter, MediaItem, MediaItemId, MediaItemRow, MediaRole};
|
|
||||||
|
|
||||||
use crate::config::LocalFilesConfig;
|
use crate::config::LocalFilesConfig;
|
||||||
use crate::index::{decode_id, LocalIndex};
|
use crate::index::{decode_id, LocalIndex};
|
||||||
use crate::scanner::LocalFileItem;
|
use crate::scanner::LocalFileItem;
|
||||||
use crate::transcoder::TranscodeManager;
|
|
||||||
|
|
||||||
pub struct LocalFilesProvider {
|
pub struct LocalFilesProvider {
|
||||||
pub index: Arc<LocalIndex>,
|
pub index: Arc<LocalIndex>,
|
||||||
base_url: String,
|
|
||||||
transcode_manager: Option<Arc<TranscodeManager>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const SHORT_DURATION_SECS: u32 = 1200;
|
const SHORT_DURATION_SECS: u32 = 1200;
|
||||||
const DECADE_SPAN: u16 = 9;
|
const DECADE_SPAN: u16 = 9;
|
||||||
|
|
||||||
impl LocalFilesProvider {
|
impl LocalFilesProvider {
|
||||||
pub fn new(
|
pub fn new(index: Arc<LocalIndex>, _config: &LocalFilesConfig) -> Self {
|
||||||
index: Arc<LocalIndex>,
|
Self { index }
|
||||||
config: &LocalFilesConfig,
|
|
||||||
transcode_manager: Option<Arc<TranscodeManager>>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
index,
|
|
||||||
base_url: config.base_url.trim_end_matches('/').to_string(),
|
|
||||||
transcode_manager,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,13 +60,7 @@ impl IMediaProvider for LocalFilesProvider {
|
|||||||
tags: true,
|
tags: true,
|
||||||
decade: true,
|
decade: true,
|
||||||
search: true,
|
search: true,
|
||||||
streaming_protocol: if self.transcode_manager.is_some() {
|
|
||||||
StreamingProtocol::Hls
|
|
||||||
} else {
|
|
||||||
StreamingProtocol::DirectFile
|
|
||||||
},
|
|
||||||
rescan: true,
|
rescan: true,
|
||||||
transcode: self.transcode_manager.is_some(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,31 +136,14 @@ impl IMediaProvider for LocalFilesProvider {
|
|||||||
.map(|item| to_media_item(item_id.clone(), &item)))
|
.map(|item| to_media_item(item_id.clone(), &item)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_stream_url(
|
async fn get_source_uri(&self, item_id: &MediaItemId) -> DomainResult<SourceUri> {
|
||||||
&self,
|
|
||||||
item_id: &MediaItemId,
|
|
||||||
quality: &StreamQuality,
|
|
||||||
) -> DomainResult<String> {
|
|
||||||
match quality {
|
|
||||||
StreamQuality::Transcode(_) if self.transcode_manager.is_some() => {
|
|
||||||
let tm = self.transcode_manager.as_ref().unwrap();
|
|
||||||
let rel = decode_id(item_id).ok_or_else(|| {
|
let rel = decode_id(item_id).ok_or_else(|| {
|
||||||
DomainError::InfrastructureError("invalid item id encoding".into())
|
DomainError::InfrastructureError("invalid item id encoding".into())
|
||||||
})?;
|
})?;
|
||||||
let src = self.index.root_dir.join(&rel);
|
let abs_path = self.index.root_dir.join(&rel);
|
||||||
tm.ensure_transcoded(item_id.as_ref(), &src).await?;
|
Ok(SourceUri::FilePath {
|
||||||
Ok(format!(
|
path: abs_path.to_string_lossy().into_owned(),
|
||||||
"{}/api/v1/files/transcode/{}/playlist.m3u8",
|
})
|
||||||
self.base_url,
|
|
||||||
item_id.as_ref()
|
|
||||||
))
|
|
||||||
}
|
|
||||||
_ => Ok(format!(
|
|
||||||
"{}/api/v1/files/stream/{}",
|
|
||||||
self.base_url,
|
|
||||||
item_id.as_ref()
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_collections(&self) -> DomainResult<Vec<Collection>> {
|
async fn list_collections(&self) -> DomainResult<Vec<Collection>> {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
use crate::common::enum_to_string;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct ProviderCapabilitiesResponse {
|
pub struct ProviderCapabilitiesResponse {
|
||||||
pub collections: bool,
|
pub collections: bool,
|
||||||
@@ -11,9 +9,7 @@ pub struct ProviderCapabilitiesResponse {
|
|||||||
pub tags: bool,
|
pub tags: bool,
|
||||||
pub decade: bool,
|
pub decade: bool,
|
||||||
pub search: bool,
|
pub search: bool,
|
||||||
pub streaming_protocol: String,
|
|
||||||
pub rescan: bool,
|
pub rescan: bool,
|
||||||
pub transcode: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<domain::ports::ProviderCapabilities> for ProviderCapabilitiesResponse {
|
impl From<domain::ports::ProviderCapabilities> for ProviderCapabilitiesResponse {
|
||||||
@@ -25,9 +21,7 @@ impl From<domain::ports::ProviderCapabilities> for ProviderCapabilitiesResponse
|
|||||||
tags: c.tags,
|
tags: c.tags,
|
||||||
decade: c.decade,
|
decade: c.decade,
|
||||||
search: c.search,
|
search: c.search,
|
||||||
streaming_protocol: enum_to_string(&c.streaming_protocol),
|
|
||||||
rescan: c.rescan,
|
rescan: c.rescan,
|
||||||
transcode: c.transcode,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,9 +41,7 @@ pub fn execute(deps: &ConfigDeps, _query: GetConfigQuery) -> SystemConfig {
|
|||||||
tags: false,
|
tags: false,
|
||||||
decade: false,
|
decade: false,
|
||||||
search: false,
|
search: false,
|
||||||
streaming_protocol: domain::ports::StreamingProtocol::DirectFile,
|
|
||||||
rescan: false,
|
rescan: false,
|
||||||
transcode: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
SystemConfig {
|
SystemConfig {
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ use async_trait::async_trait;
|
|||||||
|
|
||||||
use domain::errors::DomainResult;
|
use domain::errors::DomainResult;
|
||||||
use domain::models::MediaItem;
|
use domain::models::MediaItem;
|
||||||
use domain::ports::{
|
use domain::ports::{Collection, IProviderRegistry, ProviderCapabilities, SeriesSummary};
|
||||||
Collection, IProviderRegistry, ProviderCapabilities, SeriesSummary, StreamQuality,
|
use domain::SourceUri;
|
||||||
StreamingProtocol,
|
|
||||||
};
|
|
||||||
use domain::testing::{InMemoryLibraryRepository, NoopEventPublisher, NoopLibrarySync};
|
use domain::testing::{InMemoryLibraryRepository, NoopEventPublisher, NoopLibrarySync};
|
||||||
use domain::value_objects::{ContentType, MediaFilter, MediaItemId};
|
use domain::value_objects::{ContentType, MediaFilter, MediaItemId};
|
||||||
|
|
||||||
@@ -29,11 +27,7 @@ impl IProviderRegistry for TestProviderRegistry {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_stream_url(
|
async fn get_source_uri(&self, _item_id: &MediaItemId) -> DomainResult<SourceUri> {
|
||||||
&self,
|
|
||||||
_item_id: &MediaItemId,
|
|
||||||
_quality: &StreamQuality,
|
|
||||||
) -> DomainResult<String> {
|
|
||||||
Err(domain::DomainError::InfrastructureError(
|
Err(domain::DomainError::InfrastructureError(
|
||||||
"TestProviderRegistry does not support streaming".into(),
|
"TestProviderRegistry does not support streaming".into(),
|
||||||
))
|
))
|
||||||
@@ -55,9 +49,7 @@ impl IProviderRegistry for TestProviderRegistry {
|
|||||||
tags: false,
|
tags: false,
|
||||||
decade: false,
|
decade: false,
|
||||||
search: true,
|
search: true,
|
||||||
streaming_protocol: StreamingProtocol::Hls,
|
|
||||||
rescan: false,
|
rescan: false,
|
||||||
transcode: false,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
|
||||||
use domain::ports::StreamQuality;
|
use domain::value_objects::{ChannelId, SourceUri};
|
||||||
use domain::value_objects::ChannelId;
|
|
||||||
use domain::{DomainResult, ScheduleEngineService};
|
use domain::{DomainResult, ScheduleEngineService};
|
||||||
|
|
||||||
use super::deps::ScheduleDeps;
|
use super::deps::ScheduleDeps;
|
||||||
use super::queries::GetStreamUrlQuery;
|
use super::queries::GetSourceQuery;
|
||||||
|
|
||||||
pub async fn execute(deps: &ScheduleDeps, query: GetStreamUrlQuery) -> DomainResult<Option<String>> {
|
pub async fn execute(deps: &ScheduleDeps, query: GetSourceQuery) -> DomainResult<Option<SourceUri>> {
|
||||||
let channel_id = ChannelId::from(query.channel_id);
|
let channel_id = ChannelId::from(query.channel_id);
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
|
|
||||||
@@ -22,9 +21,9 @@ pub async fn execute(deps: &ScheduleDeps, query: GetStreamUrlQuery) -> DomainRes
|
|||||||
};
|
};
|
||||||
|
|
||||||
let item_id = broadcast.slot().item().id().clone();
|
let item_id = broadcast.slot().item().id().clone();
|
||||||
let url = deps
|
let uri = deps
|
||||||
.provider_registry
|
.provider_registry
|
||||||
.get_stream_url(&item_id, &StreamQuality::Direct)
|
.get_source_uri(&item_id)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Some(url))
|
Ok(Some(uri))
|
||||||
}
|
}
|
||||||
@@ -3,9 +3,9 @@ pub mod deps;
|
|||||||
pub mod generate;
|
pub mod generate;
|
||||||
pub mod get_current_broadcast;
|
pub mod get_current_broadcast;
|
||||||
pub mod get_epg;
|
pub mod get_epg;
|
||||||
pub mod get_stream_url;
|
pub mod get_source;
|
||||||
pub mod queries;
|
pub mod queries;
|
||||||
|
|
||||||
pub use commands::GenerateScheduleCommand;
|
pub use commands::GenerateScheduleCommand;
|
||||||
pub use deps::ScheduleDeps;
|
pub use deps::ScheduleDeps;
|
||||||
pub use queries::{GetCurrentBroadcastQuery, GetEpgQuery, GetStreamUrlQuery};
|
pub use queries::{GetCurrentBroadcastQuery, GetEpgQuery, GetSourceQuery};
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ pub struct GetEpgQuery {
|
|||||||
pub channel_id: Uuid,
|
pub channel_id: Uuid,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct GetStreamUrlQuery {
|
pub struct GetSourceQuery {
|
||||||
pub channel_id: Uuid,
|
pub channel_id: Uuid,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ use async_trait::async_trait;
|
|||||||
|
|
||||||
use domain::errors::DomainResult;
|
use domain::errors::DomainResult;
|
||||||
use domain::models::MediaItem;
|
use domain::models::MediaItem;
|
||||||
use domain::ports::{
|
use domain::ports::{Collection, IProviderRegistry, ProviderCapabilities, SeriesSummary};
|
||||||
Collection, IProviderRegistry, ProviderCapabilities,
|
use domain::SourceUri;
|
||||||
SeriesSummary, StreamQuality, StreamingProtocol,
|
|
||||||
};
|
|
||||||
use domain::testing::{InMemoryChannelRepository, InMemoryLibraryRepository, InMemoryScheduleRepository, NoopEventPublisher};
|
use domain::testing::{InMemoryChannelRepository, InMemoryLibraryRepository, InMemoryScheduleRepository, NoopEventPublisher};
|
||||||
use domain::value_objects::{ContentType, MediaFilter, MediaItemId};
|
use domain::value_objects::{ContentType, MediaFilter, MediaItemId};
|
||||||
use domain::ScheduleEngineService;
|
use domain::ScheduleEngineService;
|
||||||
@@ -30,11 +28,7 @@ impl IProviderRegistry for TestProviderRegistry {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_stream_url(
|
async fn get_source_uri(&self, _item_id: &MediaItemId) -> DomainResult<SourceUri> {
|
||||||
&self,
|
|
||||||
_item_id: &MediaItemId,
|
|
||||||
_quality: &StreamQuality,
|
|
||||||
) -> DomainResult<String> {
|
|
||||||
Err(domain::DomainError::InfrastructureError(
|
Err(domain::DomainError::InfrastructureError(
|
||||||
"TestProviderRegistry does not support streaming".into(),
|
"TestProviderRegistry does not support streaming".into(),
|
||||||
))
|
))
|
||||||
@@ -56,9 +50,7 @@ impl IProviderRegistry for TestProviderRegistry {
|
|||||||
tags: false,
|
tags: false,
|
||||||
decade: false,
|
decade: false,
|
||||||
search: false,
|
search: false,
|
||||||
streaming_protocol: StreamingProtocol::Hls,
|
|
||||||
rescan: false,
|
rescan: false,
|
||||||
transcode: false,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,20 +3,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::errors::{DomainError, DomainResult};
|
use crate::errors::{DomainError, DomainResult};
|
||||||
use crate::models::MediaItem;
|
use crate::models::MediaItem;
|
||||||
use crate::value_objects::{ContentType, MediaFilter, MediaItemId};
|
use crate::value_objects::{ContentType, MediaFilter, MediaItemId, SourceUri};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum StreamQuality {
|
|
||||||
Direct,
|
|
||||||
Transcode(u32),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum StreamingProtocol {
|
|
||||||
Hls,
|
|
||||||
DirectFile,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ProviderCapabilities {
|
pub struct ProviderCapabilities {
|
||||||
@@ -26,9 +13,7 @@ pub struct ProviderCapabilities {
|
|||||||
pub tags: bool,
|
pub tags: bool,
|
||||||
pub decade: bool,
|
pub decade: bool,
|
||||||
pub search: bool,
|
pub search: bool,
|
||||||
pub streaming_protocol: StreamingProtocol,
|
|
||||||
pub rescan: bool,
|
pub rescan: bool,
|
||||||
pub transcode: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -55,11 +40,7 @@ pub trait IMediaProvider: Send + Sync {
|
|||||||
|
|
||||||
async fn fetch_by_id(&self, item_id: &MediaItemId) -> DomainResult<Option<MediaItem>>;
|
async fn fetch_by_id(&self, item_id: &MediaItemId) -> DomainResult<Option<MediaItem>>;
|
||||||
|
|
||||||
async fn get_stream_url(
|
async fn get_source_uri(&self, item_id: &MediaItemId) -> DomainResult<SourceUri>;
|
||||||
&self,
|
|
||||||
item_id: &MediaItemId,
|
|
||||||
quality: &StreamQuality,
|
|
||||||
) -> DomainResult<String>;
|
|
||||||
|
|
||||||
async fn list_collections(&self) -> DomainResult<Vec<Collection>> {
|
async fn list_collections(&self) -> DomainResult<Vec<Collection>> {
|
||||||
Err(DomainError::InfrastructureError(
|
Err(DomainError::InfrastructureError(
|
||||||
@@ -95,11 +76,7 @@ pub trait IProviderRegistry: Send + Sync {
|
|||||||
|
|
||||||
async fn fetch_by_id(&self, item_id: &MediaItemId) -> DomainResult<Option<MediaItem>>;
|
async fn fetch_by_id(&self, item_id: &MediaItemId) -> DomainResult<Option<MediaItem>>;
|
||||||
|
|
||||||
async fn get_stream_url(
|
async fn get_source_uri(&self, item_id: &MediaItemId) -> DomainResult<SourceUri>;
|
||||||
&self,
|
|
||||||
item_id: &MediaItemId,
|
|
||||||
quality: &StreamQuality,
|
|
||||||
) -> DomainResult<String>;
|
|
||||||
|
|
||||||
fn provider_ids(&self) -> Vec<String>;
|
fn provider_ids(&self) -> Vec<String>;
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ pub use events::{DomainEvent, EventConsumer, EventEnvelope, EventHandler, EventP
|
|||||||
pub use library::{LibraryCommand, LibraryQuery, LibrarySyncAdapter};
|
pub use library::{LibraryCommand, LibraryQuery, LibrarySyncAdapter};
|
||||||
pub use media::{
|
pub use media::{
|
||||||
Collection, IMediaProvider, IProviderRegistry, ProviderCapabilities, SeriesSummary,
|
Collection, IMediaProvider, IProviderRegistry, ProviderCapabilities, SeriesSummary,
|
||||||
StreamQuality, StreamingProtocol,
|
|
||||||
};
|
};
|
||||||
pub use provider_config::{ProviderConfigCommand, ProviderConfigQuery};
|
pub use provider_config::{ProviderConfigCommand, ProviderConfigQuery};
|
||||||
pub use schedule::{ScheduleCommand, ScheduleQuery};
|
pub use schedule::{ScheduleCommand, ScheduleQuery};
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ use crate::models::{
|
|||||||
};
|
};
|
||||||
use crate::ports::{
|
use crate::ports::{
|
||||||
ActivityLogCommand, ActivityLogQuery, EventConsumer, EventPublisher, IMediaProvider,
|
ActivityLogCommand, ActivityLogQuery, EventConsumer, EventPublisher, IMediaProvider,
|
||||||
LibrarySyncAdapter, ProviderCapabilities, StreamQuality, StreamingProtocol,
|
LibrarySyncAdapter, ProviderCapabilities,
|
||||||
};
|
};
|
||||||
use crate::value_objects::{ChannelId, MediaFilter, MediaItemId};
|
use crate::value_objects::{ChannelId, MediaFilter, MediaItemId, SourceUri};
|
||||||
|
|
||||||
pub struct NoopEventPublisher;
|
pub struct NoopEventPublisher;
|
||||||
|
|
||||||
@@ -85,9 +85,7 @@ impl IMediaProvider for NoopMediaProvider {
|
|||||||
tags: false,
|
tags: false,
|
||||||
decade: false,
|
decade: false,
|
||||||
search: false,
|
search: false,
|
||||||
streaming_protocol: StreamingProtocol::Hls,
|
|
||||||
rescan: false,
|
rescan: false,
|
||||||
transcode: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,11 +97,7 @@ impl IMediaProvider for NoopMediaProvider {
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_stream_url(
|
async fn get_source_uri(&self, _item_id: &MediaItemId) -> DomainResult<SourceUri> {
|
||||||
&self,
|
|
||||||
_item_id: &MediaItemId,
|
|
||||||
_quality: &StreamQuality,
|
|
||||||
) -> DomainResult<String> {
|
|
||||||
Err(crate::errors::DomainError::InfrastructureError(
|
Err(crate::errors::DomainError::InfrastructureError(
|
||||||
"NoopMediaProvider does not support streaming".into(),
|
"NoopMediaProvider does not support streaming".into(),
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ pub mod channel;
|
|||||||
pub mod ids;
|
pub mod ids;
|
||||||
pub mod scheduling;
|
pub mod scheduling;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
|
pub mod streaming;
|
||||||
|
|
||||||
pub use auth::*;
|
pub use auth::*;
|
||||||
pub use channel::*;
|
pub use channel::*;
|
||||||
pub use ids::*;
|
pub use ids::*;
|
||||||
pub use scheduling::*;
|
pub use scheduling::*;
|
||||||
pub use search::*;
|
pub use search::*;
|
||||||
|
pub use streaming::*;
|
||||||
|
|||||||
8
crates/domain/src/value_objects/streaming.rs
Normal file
8
crates/domain/src/value_objects/streaming.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
|
pub enum SourceUri {
|
||||||
|
NetworkUrl { url: String },
|
||||||
|
FilePath { path: String },
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use domain::ports::{IMediaProvider, IProviderRegistry, ProviderCapabilities, StreamingProtocol};
|
use domain::ports::{IMediaProvider, IProviderRegistry, ProviderCapabilities};
|
||||||
use domain::ports::StreamQuality;
|
|
||||||
use domain::{DomainError, DomainResult, MediaFilter, MediaItem, MediaItemId, ScheduleEngineService};
|
use domain::{DomainError, DomainResult, MediaFilter, MediaItem, MediaItemId, ScheduleEngineService};
|
||||||
use infra_wiring::DbPool;
|
use infra_wiring::DbPool;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
@@ -175,9 +174,7 @@ impl IMediaProvider for NoopMediaProvider {
|
|||||||
tags: false,
|
tags: false,
|
||||||
decade: false,
|
decade: false,
|
||||||
search: false,
|
search: false,
|
||||||
streaming_protocol: StreamingProtocol::DirectFile,
|
|
||||||
rescan: false,
|
rescan: false,
|
||||||
transcode: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +190,7 @@ impl IMediaProvider for NoopMediaProvider {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_stream_url(&self, _: &MediaItemId, _: &StreamQuality) -> DomainResult<String> {
|
async fn get_source_uri(&self, _: &MediaItemId) -> DomainResult<domain::SourceUri> {
|
||||||
Err(DomainError::InfrastructureError(
|
Err(DomainError::InfrastructureError(
|
||||||
"No media provider configured.".into(),
|
"No media provider configured.".into(),
|
||||||
))
|
))
|
||||||
@@ -254,19 +251,18 @@ impl IProviderRegistry for SimpleProviderRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_stream_url(
|
async fn get_source_uri(
|
||||||
&self,
|
&self,
|
||||||
item_id: &MediaItemId,
|
item_id: &MediaItemId,
|
||||||
quality: &StreamQuality,
|
) -> DomainResult<domain::SourceUri> {
|
||||||
) -> DomainResult<String> {
|
|
||||||
let id_str = item_id.value();
|
let id_str = item_id.value();
|
||||||
if let Some(pid) = Self::extract_provider_id(id_str)
|
if let Some(pid) = Self::extract_provider_id(id_str)
|
||||||
&& let Some(provider) = self.get(pid)
|
&& let Some(provider) = self.get(pid)
|
||||||
{
|
{
|
||||||
return provider.get_stream_url(item_id, quality).await;
|
return provider.get_source_uri(item_id).await;
|
||||||
}
|
}
|
||||||
if let Some(provider) = self.primary() {
|
if let Some(provider) = self.primary() {
|
||||||
provider.get_stream_url(item_id, quality).await
|
provider.get_source_uri(item_id).await
|
||||||
} else {
|
} else {
|
||||||
Err(DomainError::InfrastructureError(
|
Err(DomainError::InfrastructureError(
|
||||||
"No provider available".into(),
|
"No provider available".into(),
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use application::{
|
|||||||
providers::ProviderDeps,
|
providers::ProviderDeps,
|
||||||
schedule::ScheduleDeps,
|
schedule::ScheduleDeps,
|
||||||
};
|
};
|
||||||
use domain::ports::{IMediaProvider, IProviderRegistry, ProviderCapabilities, StreamingProtocol};
|
use domain::ports::{IMediaProvider, IProviderRegistry, ProviderCapabilities};
|
||||||
use domain::{DomainError, ScheduleEngineService};
|
use domain::{DomainError, ScheduleEngineService};
|
||||||
use infra_wiring::{Config, ConfigSource, DbPool};
|
use infra_wiring::{Config, ConfigSource, DbPool};
|
||||||
|
|
||||||
@@ -267,9 +267,7 @@ impl IMediaProvider for NoopMediaProvider {
|
|||||||
tags: false,
|
tags: false,
|
||||||
decade: false,
|
decade: false,
|
||||||
search: false,
|
search: false,
|
||||||
streaming_protocol: StreamingProtocol::DirectFile,
|
|
||||||
rescan: false,
|
rescan: false,
|
||||||
transcode: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,11 +289,7 @@ impl IMediaProvider for NoopMediaProvider {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_stream_url(
|
async fn get_source_uri(&self, _: &domain::MediaItemId) -> domain::DomainResult<domain::SourceUri> {
|
||||||
&self,
|
|
||||||
_: &domain::MediaItemId,
|
|
||||||
_: &domain::ports::StreamQuality,
|
|
||||||
) -> domain::DomainResult<String> {
|
|
||||||
Err(DomainError::InfrastructureError(
|
Err(DomainError::InfrastructureError(
|
||||||
"No media provider configured.".into(),
|
"No media provider configured.".into(),
|
||||||
))
|
))
|
||||||
@@ -359,19 +353,18 @@ impl IProviderRegistry for SimpleProviderRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_stream_url(
|
async fn get_source_uri(
|
||||||
&self,
|
&self,
|
||||||
item_id: &domain::MediaItemId,
|
item_id: &domain::MediaItemId,
|
||||||
quality: &domain::ports::StreamQuality,
|
) -> domain::DomainResult<domain::SourceUri> {
|
||||||
) -> domain::DomainResult<String> {
|
|
||||||
let id_str = item_id.value();
|
let id_str = item_id.value();
|
||||||
if let Some(pid) = Self::extract_provider_id(id_str) {
|
if let Some(pid) = Self::extract_provider_id(id_str) {
|
||||||
if let Some(provider) = self.get(pid) {
|
if let Some(provider) = self.get(pid) {
|
||||||
return provider.get_stream_url(item_id, quality).await;
|
return provider.get_source_uri(item_id).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(provider) = self.primary() {
|
if let Some(provider) = self.primary() {
|
||||||
provider.get_stream_url(item_id, quality).await
|
provider.get_source_uri(item_id).await
|
||||||
} else {
|
} else {
|
||||||
Err(DomainError::InfrastructureError(
|
Err(DomainError::InfrastructureError(
|
||||||
"No provider available".into(),
|
"No provider available".into(),
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use api_types::{
|
|||||||
CurrentBroadcastResponse, ScheduleHistoryEntry, ScheduleResponse, SlotResponse,
|
CurrentBroadcastResponse, ScheduleHistoryEntry, ScheduleResponse, SlotResponse,
|
||||||
};
|
};
|
||||||
use application::schedule::{
|
use application::schedule::{
|
||||||
GenerateScheduleCommand, GetCurrentBroadcastQuery, GetEpgQuery, GetStreamUrlQuery,
|
GenerateScheduleCommand, GetCurrentBroadcastQuery, GetEpgQuery, GetSourceQuery,
|
||||||
};
|
};
|
||||||
use domain::value_objects::ChannelId;
|
use domain::value_objects::ChannelId;
|
||||||
|
|
||||||
@@ -70,13 +70,9 @@ pub async fn get_stream(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(id): Path<uuid::Uuid>,
|
Path(id): Path<uuid::Uuid>,
|
||||||
) -> Result<axum::response::Response, AppError> {
|
) -> Result<axum::response::Response, AppError> {
|
||||||
let query = GetStreamUrlQuery { channel_id: id };
|
let query = GetSourceQuery { channel_id: id };
|
||||||
match application::schedule::get_stream_url::execute(&state.schedule_deps, query).await? {
|
match application::schedule::get_source::execute(&state.schedule_deps, query).await? {
|
||||||
Some(url) => Ok((
|
Some(uri) => Ok(Json(uri).into_response()),
|
||||||
StatusCode::TEMPORARY_REDIRECT,
|
|
||||||
[("Location", url.as_str())],
|
|
||||||
)
|
|
||||||
.into_response()),
|
|
||||||
None => Ok(StatusCode::NO_CONTENT.into_response()),
|
None => Ok(StatusCode::NO_CONTENT.into_response()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,9 +86,7 @@ impl domain::ports::IMediaProvider for RegistryProviderAdapter {
|
|||||||
tags: false,
|
tags: false,
|
||||||
decade: false,
|
decade: false,
|
||||||
search: false,
|
search: false,
|
||||||
streaming_protocol: domain::ports::StreamingProtocol::DirectFile,
|
|
||||||
rescan: false,
|
rescan: false,
|
||||||
transcode: false,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,11 +104,10 @@ impl domain::ports::IMediaProvider for RegistryProviderAdapter {
|
|||||||
self.registry.fetch_by_id(item_id).await
|
self.registry.fetch_by_id(item_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_stream_url(
|
async fn get_source_uri(
|
||||||
&self,
|
&self,
|
||||||
item_id: &domain::MediaItemId,
|
item_id: &domain::MediaItemId,
|
||||||
quality: &domain::ports::StreamQuality,
|
) -> domain::DomainResult<domain::SourceUri> {
|
||||||
) -> domain::DomainResult<String> {
|
self.registry.get_source_uri(item_id).await
|
||||||
self.registry.get_stream_url(item_id, quality).await
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use application::schedule::ScheduleDeps;
|
use application::schedule::ScheduleDeps;
|
||||||
use domain::ports::{IMediaProvider, IProviderRegistry, ProviderCapabilities, StreamingProtocol};
|
use domain::ports::{IMediaProvider, IProviderRegistry, ProviderCapabilities};
|
||||||
use domain::{DomainError, ScheduleEngineService};
|
use domain::{DomainError, ScheduleEngineService};
|
||||||
use infra_wiring::{Config, ConfigSource, DbPool};
|
use infra_wiring::{Config, ConfigSource, DbPool};
|
||||||
|
|
||||||
@@ -170,9 +170,7 @@ impl IMediaProvider for NoopMediaProvider {
|
|||||||
tags: false,
|
tags: false,
|
||||||
decade: false,
|
decade: false,
|
||||||
search: false,
|
search: false,
|
||||||
streaming_protocol: StreamingProtocol::DirectFile,
|
|
||||||
rescan: false,
|
rescan: false,
|
||||||
transcode: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,11 +192,10 @@ impl IMediaProvider for NoopMediaProvider {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_stream_url(
|
async fn get_source_uri(
|
||||||
&self,
|
&self,
|
||||||
_: &domain::MediaItemId,
|
_: &domain::MediaItemId,
|
||||||
_: &domain::ports::StreamQuality,
|
) -> domain::DomainResult<domain::SourceUri> {
|
||||||
) -> domain::DomainResult<String> {
|
|
||||||
Err(DomainError::InfrastructureError(
|
Err(DomainError::InfrastructureError(
|
||||||
"No media provider configured.".into(),
|
"No media provider configured.".into(),
|
||||||
))
|
))
|
||||||
@@ -262,19 +259,18 @@ impl IProviderRegistry for SimpleProviderRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_stream_url(
|
async fn get_source_uri(
|
||||||
&self,
|
&self,
|
||||||
item_id: &domain::MediaItemId,
|
item_id: &domain::MediaItemId,
|
||||||
quality: &domain::ports::StreamQuality,
|
) -> domain::DomainResult<domain::SourceUri> {
|
||||||
) -> domain::DomainResult<String> {
|
|
||||||
let id_str = item_id.value();
|
let id_str = item_id.value();
|
||||||
if let Some(pid) = Self::extract_provider_id(id_str) {
|
if let Some(pid) = Self::extract_provider_id(id_str) {
|
||||||
if let Some(provider) = self.get(pid) {
|
if let Some(provider) = self.get(pid) {
|
||||||
return provider.get_stream_url(item_id, quality).await;
|
return provider.get_source_uri(item_id).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(provider) = self.primary() {
|
if let Some(provider) = self.primary() {
|
||||||
provider.get_stream_url(item_id, quality).await
|
provider.get_source_uri(item_id).await
|
||||||
} else {
|
} else {
|
||||||
Err(DomainError::InfrastructureError(
|
Err(DomainError::InfrastructureError(
|
||||||
"No provider available".into(),
|
"No provider available".into(),
|
||||||
|
|||||||
Reference in New Issue
Block a user