domain: flip catalog frontend off Jellyfin item-type strings (phase 2a)

Migrate catalog MediaItem consumers from stringly item.type ("Audio",
"MusicAlbum", …) to the neutral item.kind enum across all classification
logic: home, library detail, player routing, artist/person/related/genre
components, tv store.

Model refinements found during migration (each a real distinction the
flat item_type collapsed):
- MediaKind::LiveChannel — live TV (playable, non-seekable) vs
- MediaKind::ChannelItem — channel VOD leaf (playable, seekable) vs
- MediaKind::Channel — channel container (drill-in).
  TvChannel->LiveChannel, non-folder ChannelFolderItem->ChannelItem.

RelatedItemsSection and GenreTags props migrated from Jellyfin type
strings to MediaKind; MediaKind re-exported from api/types.

Deferred by design: display {item.type} text, ResultsCounter labels,
Person.type (role), stream.type (phase 4), and all runTimeTicks/tick math
(coupled to playbackPositionTicks — phase 3). Old fields still dual-carried
so nothing breaks.

Rust 456 + 7 domain tests, frontend 644 tests, check clean.
This commit is contained in:
2026-07-23 21:12:20 +02:00
parent 55fa26377a
commit 772e9ca6d5
15 changed files with 167 additions and 134 deletions
+10 -7
View File
@@ -50,18 +50,20 @@ pub fn kind_from_jellyfin(item_type: &str, is_folder: bool) -> MediaKind {
MediaKind::Person
}
// Live TV / channels
"TvChannel" | "LiveTvChannel" | "Channel" => MediaKind::Channel,
// A live TV channel: playable, but a non-seekable live stream.
"TvChannel" | "LiveTvChannel" => MediaKind::LiveChannel,
// A bare channel is a container the user drills into.
"Channel" => MediaKind::Channel,
// Containers
"Folder" | "CollectionFolder" | "UserView" | "BoxSet" => MediaKind::Folder,
// ChannelFolderItem is a container when it is a folder, else a leaf we
// do not model further.
// ChannelFolderItem is a container when it is a folder, else a playable
// channel leaf (distinct kind so the UI can route it to playback).
"ChannelFolderItem" => {
if is_folder {
MediaKind::Folder
} else {
MediaKind::Other
MediaKind::ChannelItem
}
}
@@ -117,7 +119,8 @@ mod tests {
#[test]
fn channel_and_container_types_map() {
assert_eq!(kind_from_jellyfin("TvChannel", false), MediaKind::Channel);
assert_eq!(kind_from_jellyfin("TvChannel", false), MediaKind::LiveChannel);
assert_eq!(kind_from_jellyfin("Channel", false), MediaKind::Channel);
assert_eq!(
kind_from_jellyfin("CollectionFolder", true),
MediaKind::Folder
@@ -133,7 +136,7 @@ mod tests {
);
assert_eq!(
kind_from_jellyfin("ChannelFolderItem", false),
MediaKind::Other
MediaKind::ChannelItem
);
}
+10
View File
@@ -36,8 +36,18 @@ pub enum MediaKind {
// Cast/crew
Person,
// Containers / live TV
/// A channel *container* the user drills into (Jellyfin `Channel`).
Channel,
Folder,
/// A live TV channel — playable, but a live stream with no seekable
/// timeline (no resume/seek). Jellyfin `TvChannel`/`LiveTvChannel`.
LiveChannel,
/// A playable leaf inside a channel (Jellyfin `ChannelFolderItem` that is
/// not itself a folder) — e.g. a plugin-channel VOD item that has no
/// dedicated item type but carries its own media streams. Playable and
/// seekable, unlike `LiveChannel`. Distinct from `Channel` (the container)
/// and from `Other` so the UI can route it to playback.
ChannelItem,
/// A kind we do not model explicitly. Reached only for provider item types
/// that map to nothing meaningful; consumers treat it like an opaque
/// container. The mapping must be *total* — it never panics — so this is the