domain: neutral StreamKind for media streams (phase 4d)

Add StreamKind enum (audio/video/subtitle/other) to the domain module with
a total stream_kind_from_jellyfin mapper. MediaStream gains a kind field
(dual-carry), populated at the mapping seam. Frontend VideoPlayer track/
subtitle selection and the channel-video check now use stream.kind instead
of the Jellyfin stream.type string.

Rust 456 (+ stream_kinds_map test), frontend 644, check clean.
This commit is contained in:
2026-07-23 22:11:31 +02:00
parent ec8a7610f5
commit 1968c06172
8 changed files with 68 additions and 9 deletions
+19 -1
View File
@@ -1912,7 +1912,16 @@ export type MediaSource = { id: string; name: string; container?: string | null;
/**
* Media stream information (audio, video, subtitle tracks)
*/
export type MediaStream = { type: string; codec?: string | null; language?: string | null; displayTitle?: string | null; index: number; isDefault: boolean; isForced: boolean }
export type MediaStream = {
/**
* Legacy Jellyfin stream type string ("Audio"/"Video"/"Subtitle"). Being
* replaced by `kind`; dual-carried while the frontend migrates.
*/
type: string;
/**
* Provider-neutral stream classification — replaces `stream_type`.
*/
kind?: StreamKind; codec?: string | null; language?: string | null; displayTitle?: string | null; index: number; isDefault: boolean; isForced: boolean }
export type MediaType = "audio" | "video"
/**
* Lightweight media item for merged playback state
@@ -2516,6 +2525,15 @@ export type SmartCacheStats = { total_size: number; storage_limit: number; avail
* Storage statistics for downloads
*/
export type StorageStats = { total_bytes: number; total_items: number; albums: AlbumStorageInfo[] }
/**
* The kind of a media stream within an item (audio track, video track,
* subtitle, …) — provider-neutral, replacing the stringly Jellyfin stream type.
*/
export type StreamKind = "audio" | "video" | "subtitle" |
/**
* Any stream kind we do not model explicitly (e.g. embedded image, data).
*/
"other"
/**
* Represents a subtitle track
*/
+3 -3
View File
@@ -180,7 +180,7 @@
console.log("[VideoPlayer] No media or mediaStreams available");
return [];
}
const tracks = media.mediaStreams.filter(stream => stream.type === "Audio");
const tracks = media.mediaStreams.filter(stream => stream.kind === "audio");
console.log("[VideoPlayer] Found audio tracks:", tracks.length, tracks);
return tracks;
});
@@ -243,7 +243,7 @@
console.log("[VideoPlayer] No media or mediaStreams available for subtitles");
return [];
}
const tracks = media.mediaStreams.filter(stream => stream.type === "Subtitle");
const tracks = media.mediaStreams.filter(stream => stream.kind === "subtitle");
console.log("[VideoPlayer] Found subtitle tracks:", tracks.length, tracks);
return tracks;
});
@@ -520,7 +520,7 @@
// Build subtitle tracks for native player
const subtitleTracks = [];
if (media.mediaStreams && mediaSourceId) {
const subtitles = media.mediaStreams.filter(s => s.type === "Subtitle");
const subtitles = media.mediaStreams.filter(s => s.kind === "subtitle");
for (const sub of subtitles) {
try {
const url = await getSubtitleUrl(sub.index);
+1 -2
View File
@@ -111,8 +111,7 @@
function isVideoChannelItem(item: MediaItem): boolean {
return (
item.kind === "channelItem" &&
// stream.type is Jellyfin stream vocabulary (migrated in a later phase).
(item.mediaStreams?.some((s) => s.type === "Video") ?? false)
(item.mediaStreams?.some((s) => s.kind === "video") ?? false)
);
}