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:
@@ -5,7 +5,7 @@
|
||||
import { convertFileSrc } from "@tauri-apps/api/core";
|
||||
import { commands } from "$lib/api/bindings";
|
||||
import type { PlayQueueRequest } from "$lib/api/bindings";
|
||||
import type { MediaItem } from "$lib/api/types";
|
||||
import type { MediaItem, MediaKind } from "$lib/api/types";
|
||||
import { auth } from "$lib/stores/auth";
|
||||
import { library } from "$lib/stores/library";
|
||||
import { queue, currentQueueItem, isShuffle, repeatMode, hasNext as hasNextStore, hasPrevious as hasPreviousStore } from "$lib/stores/queue";
|
||||
@@ -100,7 +100,7 @@
|
||||
// Only for audio content - video content uses direct loading and shouldn't be affected by audio queue
|
||||
$effect(() => {
|
||||
const queueItem = $currentQueueItem;
|
||||
const currentIsVideo = currentMedia?.type === "Movie" || currentMedia?.type === "Episode";
|
||||
const currentIsVideo = currentMedia?.kind === "movie" || currentMedia?.kind === "episode";
|
||||
if (queueItem && queueItem.id !== currentMedia?.id && !currentIsVideo) {
|
||||
currentMedia = queueItem;
|
||||
}
|
||||
@@ -110,7 +110,8 @@
|
||||
// treat it as video when it carries a video media stream.
|
||||
function isVideoChannelItem(item: MediaItem): boolean {
|
||||
return (
|
||||
item.type === "ChannelFolderItem" &&
|
||||
item.kind === "channelItem" &&
|
||||
// stream.type is Jellyfin stream vocabulary (migrated in a later phase).
|
||||
(item.mediaStreams?.some((s) => s.type === "Video") ?? false)
|
||||
);
|
||||
}
|
||||
@@ -125,14 +126,13 @@
|
||||
console.log("loadAndPlay: Loading item", id);
|
||||
// Load item details
|
||||
const item = await library.loadItem(id);
|
||||
console.log("loadAndPlay: Loaded item", item.name, "type:", item.type);
|
||||
console.log("loadAndPlay: Loaded item", item.name, "kind:", item.kind);
|
||||
currentMedia = item;
|
||||
|
||||
// Check if this is a non-playable collection type that should be viewed in library instead
|
||||
const collectionTypes = ["MusicAlbum", "MusicArtist", "Series", "Season", "Folder", "CollectionFolder", "Playlist", "Channel"];
|
||||
// A ChannelFolderItem that is itself a folder is a container, not playable.
|
||||
if (collectionTypes.includes(item.type) || (item.type === "ChannelFolderItem" && item.isFolder)) {
|
||||
console.log("loadAndPlay: Redirecting collection type to library:", item.type);
|
||||
const collectionKinds: MediaKind[] = ["album", "artist", "series", "season", "folder", "playlist", "channel"];
|
||||
if (item.kind && collectionKinds.includes(item.kind)) {
|
||||
console.log("loadAndPlay: Redirecting collection type to library:", item.kind);
|
||||
goto(`/library/${id}`);
|
||||
return;
|
||||
}
|
||||
@@ -144,8 +144,8 @@
|
||||
const alreadyPlayingMedia = get(storeCurrentMedia);
|
||||
if (alreadyPlayingMedia?.id === id && !startPosition && !forceRestart) {
|
||||
console.log("loadAndPlay: Track already playing, showing UI without restarting");
|
||||
isLive = item.type === "TvChannel";
|
||||
isVideo = item.type === "Movie" || item.type === "Episode" || isLive || isVideoChannelItem(item);
|
||||
isLive = item.kind === "liveChannel";
|
||||
isVideo = item.kind === "movie" || item.kind === "episode" || isLive || isVideoChannelItem(item);
|
||||
isPlaying = true;
|
||||
loading = false;
|
||||
// hasNext/hasPrevious come from the event-driven queue store.
|
||||
@@ -158,8 +158,8 @@
|
||||
|
||||
// Determine if this is video content (Movie, Episode, live TV channels, and
|
||||
// channel leaf items that carry a video stream).
|
||||
isLive = item.type === "TvChannel";
|
||||
isVideo = item.type === "Movie" || item.type === "Episode" || isLive || isVideoChannelItem(item);
|
||||
isLive = item.kind === "liveChannel";
|
||||
isVideo = item.kind === "movie" || item.kind === "episode" || isLive || isVideoChannelItem(item);
|
||||
|
||||
// When switching to video, stop audio playback and clear the queue
|
||||
// This prevents audio from continuing in the background and clears stale state
|
||||
@@ -330,7 +330,7 @@
|
||||
sortOrder: "Ascending",
|
||||
limit: 500,
|
||||
});
|
||||
const audioTracks = result.items.filter(t => t.type === "Audio");
|
||||
const audioTracks = result.items.filter(t => t.kind === "track");
|
||||
|
||||
if (audioTracks.length > 0) {
|
||||
// Find the index of the current item in the tracks
|
||||
@@ -428,7 +428,7 @@
|
||||
loading = false;
|
||||
|
||||
// Fetch next episode for video episodes (for skip button)
|
||||
console.log("[NextEpisode] Post-load check: isVideo=", isVideo, "currentMedia=", currentMedia?.type, currentMedia?.name);
|
||||
console.log("[NextEpisode] Post-load check: isVideo=", isVideo, "currentMedia=", currentMedia?.kind, currentMedia?.name);
|
||||
if (isVideo && currentMedia) {
|
||||
fetchNextEpisode(currentMedia);
|
||||
} else {
|
||||
@@ -566,8 +566,8 @@
|
||||
|
||||
async function fetchNextEpisode(media: MediaItem) {
|
||||
nextEpisode = null;
|
||||
console.log("[NextEpisode] fetchNextEpisode called:", { type: media.type, seriesId: media.seriesId, seasonId: media.seasonId, indexNumber: media.indexNumber, id: media.id, name: media.name });
|
||||
if (media.type !== "Episode" || !media.seasonId || media.indexNumber == null) {
|
||||
console.log("[NextEpisode] fetchNextEpisode called:", { kind: media.kind, seriesId: media.seriesId, seasonId: media.seasonId, indexNumber: media.indexNumber, id: media.id, name: media.name });
|
||||
if (media.kind !== "episode" || !media.seasonId || media.indexNumber == null) {
|
||||
console.log("[NextEpisode] Skipping - not an episode or missing seasonId/indexNumber");
|
||||
return;
|
||||
}
|
||||
@@ -575,7 +575,7 @@
|
||||
const repo = auth.getRepository();
|
||||
// Fetch all episodes in the season sorted by episode number
|
||||
const result = await repo.getItems(media.seasonId, { sortBy: "IndexNumber", sortOrder: "Ascending", limit: 500 });
|
||||
const episodes = result.items.filter(e => e.type === "Episode");
|
||||
const episodes = result.items.filter(e => e.kind === "episode");
|
||||
console.log("[NextEpisode] Season has", episodes.length, "episodes, current index:", media.indexNumber);
|
||||
|
||||
// Find the episode after the current one by index number
|
||||
|
||||
Reference in New Issue
Block a user