feat(library and playback): Support for serverside channel plugins and hls streaming
This commit is contained in:
@@ -363,6 +363,8 @@ struct JellyfinItem {
|
||||
name: String,
|
||||
#[serde(rename = "Type")]
|
||||
item_type: String,
|
||||
#[serde(default)]
|
||||
is_folder: bool,
|
||||
parent_id: Option<String>,
|
||||
overview: Option<String>,
|
||||
genres: Option<Vec<String>>,
|
||||
@@ -450,6 +452,7 @@ impl JellyfinItem {
|
||||
id: self.id,
|
||||
name: self.name,
|
||||
item_type: self.item_type,
|
||||
is_folder: self.is_folder,
|
||||
server_id,
|
||||
parent_id: self.parent_id,
|
||||
library_id: None, // Not provided by Jellyfin API directly
|
||||
@@ -728,6 +731,7 @@ impl MediaRepository for OnlineRepository {
|
||||
id: album_id,
|
||||
name: first_track.album_name.clone().unwrap_or_else(|| "Unknown Album".to_string()),
|
||||
item_type: "MusicAlbum".to_string(),
|
||||
is_folder: true,
|
||||
server_id: first_track.server_id.clone(),
|
||||
parent_id: None,
|
||||
library_id: None,
|
||||
@@ -1139,6 +1143,105 @@ impl MediaRepository for OnlineRepository {
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
async fn get_live_tv_channels(&self) -> Result<Vec<MediaItem>, RepoError> {
|
||||
// Live TV channels (broadcast tuners / IPTV M3U). Returned as items with
|
||||
// type "TvChannel" — playable via open_live_stream.
|
||||
let endpoint = format!(
|
||||
"/LiveTv/Channels?UserId={}&Fields=PrimaryImageAspectRatio,Overview&EnableImageTypes=Primary",
|
||||
self.user_id
|
||||
);
|
||||
let response: ItemsResponse = self.get_json(&endpoint).await?;
|
||||
Ok(response
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|item| item.to_media_item(self.server_url.clone()))
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn get_channels(&self) -> Result<SearchResult, RepoError> {
|
||||
// Root list of plugin "Channels". Drill-down into a channel folder reuses
|
||||
// get_items(channel_id, ...).
|
||||
let endpoint = format!("/Channels?UserId={}", self.user_id);
|
||||
let response: ItemsResponse = self.get_json(&endpoint).await?;
|
||||
let total = response.total_record_count;
|
||||
let items = response
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|item| item.to_media_item(self.server_url.clone()))
|
||||
.collect();
|
||||
Ok(SearchResult {
|
||||
items,
|
||||
total_record_count: total,
|
||||
})
|
||||
}
|
||||
|
||||
async fn open_live_stream(&self, item_id: &str) -> Result<LiveStreamInfo, RepoError> {
|
||||
// Live channels require a PlaybackInfo call with AutoOpenLiveStream so the
|
||||
// server opens the live stream and returns a ready-to-play transcoding URL.
|
||||
// We send a minimal request; the server applies its own defaults for live.
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct OpenLiveStreamRequest {
|
||||
user_id: String,
|
||||
#[serde(rename = "AutoOpenLiveStream")]
|
||||
auto_open_live_stream: bool,
|
||||
is_playback: bool,
|
||||
max_streaming_bitrate: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct OpenLiveStreamResponse {
|
||||
#[serde(default)]
|
||||
media_sources: Vec<LiveMediaSource>,
|
||||
play_session_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct LiveMediaSource {
|
||||
id: String,
|
||||
transcoding_url: Option<String>,
|
||||
live_stream_id: Option<String>,
|
||||
}
|
||||
|
||||
let endpoint = format!("/Items/{}/PlaybackInfo", item_id);
|
||||
let request = OpenLiveStreamRequest {
|
||||
user_id: self.user_id.clone(),
|
||||
auto_open_live_stream: true,
|
||||
is_playback: true,
|
||||
max_streaming_bitrate: 20_000_000,
|
||||
};
|
||||
|
||||
let response: OpenLiveStreamResponse =
|
||||
self.post_json_response(&endpoint, &request).await?;
|
||||
|
||||
let source = response.media_sources.into_iter().next().ok_or(RepoError::NotFound {
|
||||
message: "No live media source returned".to_string(),
|
||||
})?;
|
||||
|
||||
// The transcoding URL is server-relative; make it absolute. If the server
|
||||
// did not provide one (rare for live), fall back to the HLS master endpoint.
|
||||
let stream_url = match source.transcoding_url {
|
||||
Some(url) => format!("{}{}", self.server_url, url),
|
||||
None => format!(
|
||||
"{}/Videos/{}/master.m3u8?api_key={}&MediaSourceId={}&LiveStreamId={}&VideoCodec=h264&AudioCodec=aac&TranscodingProtocol=hls&TranscodingContainer=ts",
|
||||
self.server_url,
|
||||
item_id,
|
||||
self.access_token,
|
||||
source.id,
|
||||
source.live_stream_id.clone().unwrap_or_default(),
|
||||
),
|
||||
};
|
||||
|
||||
Ok(LiveStreamInfo {
|
||||
stream_url,
|
||||
play_session_id: response.play_session_id,
|
||||
live_stream_id: source.live_stream_id,
|
||||
media_source_id: Some(source.id),
|
||||
})
|
||||
}
|
||||
|
||||
async fn report_playback_start(
|
||||
&self,
|
||||
item_id: &str,
|
||||
|
||||
Reference in New Issue
Block a user