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>,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
match ct {
|
||||
ContentType::Movie => "Movie",
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use domain::ports::{
|
||||
Collection, IMediaProvider, ProviderCapabilities, SeriesSummary, StreamQuality,
|
||||
StreamingProtocol,
|
||||
};
|
||||
use domain::{ContentType, DomainError, DomainResult, MediaFilter, MediaItem, MediaItemId};
|
||||
use domain::ports::{Collection, IMediaProvider, ProviderCapabilities, SeriesSummary};
|
||||
use domain::{ContentType, DomainError, DomainResult, MediaFilter, MediaItem, MediaItemId, SourceUri};
|
||||
|
||||
use crate::config::JellyfinConfig;
|
||||
use crate::mapping::{map_jellyfin_item, TICKS_PER_SEC};
|
||||
use crate::models::{
|
||||
jellyfin_item_type, JellyfinItemsResponse, JellyfinPlaybackInfoResponse,
|
||||
};
|
||||
|
||||
const FALLBACK_HLS_BITRATE: u32 = 8_000_000;
|
||||
use crate::models::{jellyfin_item_type, JellyfinItemsResponse};
|
||||
|
||||
pub struct JellyfinMediaProvider {
|
||||
client: reqwest::Client,
|
||||
@@ -129,16 +122,6 @@ impl JellyfinMediaProvider {
|
||||
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]
|
||||
@@ -151,9 +134,7 @@ impl IMediaProvider for JellyfinMediaProvider {
|
||||
tags: true,
|
||||
decade: true,
|
||||
search: true,
|
||||
streaming_protocol: StreamingProtocol::Hls,
|
||||
rescan: false,
|
||||
transcode: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,52 +331,14 @@ impl IMediaProvider for JellyfinMediaProvider {
|
||||
Ok(body.items.into_iter().map(|item| item.name).collect())
|
||||
}
|
||||
|
||||
async fn get_stream_url(
|
||||
&self,
|
||||
item_id: &MediaItemId,
|
||||
quality: &StreamQuality,
|
||||
) -> DomainResult<String> {
|
||||
match quality {
|
||||
StreamQuality::Direct => {
|
||||
let url = format!(
|
||||
"{}/Items/{}/PlaybackInfo",
|
||||
self.config.base_url,
|
||||
item_id.as_ref()
|
||||
);
|
||||
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)),
|
||||
}
|
||||
async fn get_source_uri(&self, item_id: &MediaItemId) -> DomainResult<SourceUri> {
|
||||
Ok(SourceUri::NetworkUrl {
|
||||
url: format!(
|
||||
"{}/Videos/{}/stream?static=true&api_key={}",
|
||||
self.config.base_url,
|
||||
item_id.as_ref(),
|
||||
self.config.api_key,
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ impl LocalFilesBundle {
|
||||
});
|
||||
|
||||
let provider =
|
||||
LocalFilesProvider::new(Arc::clone(&local_index), &config, transcode_manager.clone());
|
||||
LocalFilesProvider::new(Arc::clone(&local_index), &config);
|
||||
|
||||
Self {
|
||||
provider,
|
||||
|
||||
@@ -1,36 +1,23 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use domain::ports::{
|
||||
Collection, IMediaProvider, ProviderCapabilities, StreamQuality, StreamingProtocol,
|
||||
};
|
||||
use domain::{ContentType, DomainError, DomainResult, MediaFilter, MediaItem, MediaItemId, MediaItemRow, MediaRole};
|
||||
use domain::ports::{Collection, IMediaProvider, ProviderCapabilities};
|
||||
use domain::{ContentType, DomainError, DomainResult, MediaFilter, MediaItem, MediaItemId, MediaItemRow, MediaRole, SourceUri};
|
||||
|
||||
use crate::config::LocalFilesConfig;
|
||||
use crate::index::{decode_id, LocalIndex};
|
||||
use crate::scanner::LocalFileItem;
|
||||
use crate::transcoder::TranscodeManager;
|
||||
|
||||
pub struct LocalFilesProvider {
|
||||
pub index: Arc<LocalIndex>,
|
||||
base_url: String,
|
||||
transcode_manager: Option<Arc<TranscodeManager>>,
|
||||
}
|
||||
|
||||
const SHORT_DURATION_SECS: u32 = 1200;
|
||||
const DECADE_SPAN: u16 = 9;
|
||||
|
||||
impl LocalFilesProvider {
|
||||
pub fn new(
|
||||
index: Arc<LocalIndex>,
|
||||
config: &LocalFilesConfig,
|
||||
transcode_manager: Option<Arc<TranscodeManager>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
index,
|
||||
base_url: config.base_url.trim_end_matches('/').to_string(),
|
||||
transcode_manager,
|
||||
}
|
||||
pub fn new(index: Arc<LocalIndex>, _config: &LocalFilesConfig) -> Self {
|
||||
Self { index }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,13 +60,7 @@ impl IMediaProvider for LocalFilesProvider {
|
||||
tags: true,
|
||||
decade: true,
|
||||
search: true,
|
||||
streaming_protocol: if self.transcode_manager.is_some() {
|
||||
StreamingProtocol::Hls
|
||||
} else {
|
||||
StreamingProtocol::DirectFile
|
||||
},
|
||||
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)))
|
||||
}
|
||||
|
||||
async fn get_stream_url(
|
||||
&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(|| {
|
||||
DomainError::InfrastructureError("invalid item id encoding".into())
|
||||
})?;
|
||||
let src = self.index.root_dir.join(&rel);
|
||||
tm.ensure_transcoded(item_id.as_ref(), &src).await?;
|
||||
Ok(format!(
|
||||
"{}/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 get_source_uri(&self, item_id: &MediaItemId) -> DomainResult<SourceUri> {
|
||||
let rel = decode_id(item_id).ok_or_else(|| {
|
||||
DomainError::InfrastructureError("invalid item id encoding".into())
|
||||
})?;
|
||||
let abs_path = self.index.root_dir.join(&rel);
|
||||
Ok(SourceUri::FilePath {
|
||||
path: abs_path.to_string_lossy().into_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn list_collections(&self) -> DomainResult<Vec<Collection>> {
|
||||
|
||||
Reference in New Issue
Block a user