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:
@@ -50,18 +50,20 @@ pub fn kind_from_jellyfin(item_type: &str, is_folder: bool) -> MediaKind {
|
|||||||
MediaKind::Person
|
MediaKind::Person
|
||||||
}
|
}
|
||||||
|
|
||||||
// Live TV / channels
|
// A live TV channel: playable, but a non-seekable live stream.
|
||||||
"TvChannel" | "LiveTvChannel" | "Channel" => MediaKind::Channel,
|
"TvChannel" | "LiveTvChannel" => MediaKind::LiveChannel,
|
||||||
|
// A bare channel is a container the user drills into.
|
||||||
|
"Channel" => MediaKind::Channel,
|
||||||
|
|
||||||
// Containers
|
// Containers
|
||||||
"Folder" | "CollectionFolder" | "UserView" | "BoxSet" => MediaKind::Folder,
|
"Folder" | "CollectionFolder" | "UserView" | "BoxSet" => MediaKind::Folder,
|
||||||
// ChannelFolderItem is a container when it is a folder, else a leaf we
|
// ChannelFolderItem is a container when it is a folder, else a playable
|
||||||
// do not model further.
|
// channel leaf (distinct kind so the UI can route it to playback).
|
||||||
"ChannelFolderItem" => {
|
"ChannelFolderItem" => {
|
||||||
if is_folder {
|
if is_folder {
|
||||||
MediaKind::Folder
|
MediaKind::Folder
|
||||||
} else {
|
} else {
|
||||||
MediaKind::Other
|
MediaKind::ChannelItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +119,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn channel_and_container_types_map() {
|
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!(
|
assert_eq!(
|
||||||
kind_from_jellyfin("CollectionFolder", true),
|
kind_from_jellyfin("CollectionFolder", true),
|
||||||
MediaKind::Folder
|
MediaKind::Folder
|
||||||
@@ -133,7 +136,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
kind_from_jellyfin("ChannelFolderItem", false),
|
kind_from_jellyfin("ChannelFolderItem", false),
|
||||||
MediaKind::Other
|
MediaKind::ChannelItem
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,8 +36,18 @@ pub enum MediaKind {
|
|||||||
// Cast/crew
|
// Cast/crew
|
||||||
Person,
|
Person,
|
||||||
// Containers / live TV
|
// Containers / live TV
|
||||||
|
/// A channel *container* the user drills into (Jellyfin `Channel`).
|
||||||
Channel,
|
Channel,
|
||||||
Folder,
|
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
|
/// A kind we do not model explicitly. Reached only for provider item types
|
||||||
/// that map to nothing meaningful; consumers treat it like an opaque
|
/// that map to nothing meaningful; consumers treat it like an opaque
|
||||||
/// container. The mapping must be *total* — it never panics — so this is the
|
/// container. The mapping must be *total* — it never panics — so this is the
|
||||||
|
|||||||
+18
-1
@@ -1858,7 +1858,24 @@ imageId?: string | null; backdropImageTags?: string[] | null; parentBackdropImag
|
|||||||
* typo or an unhandled kind is a compile error on the frontend, not a silent
|
* typo or an unhandled kind is a compile error on the frontend, not a silent
|
||||||
* runtime miss across ~127 comparison sites.
|
* runtime miss across ~127 comparison sites.
|
||||||
*/
|
*/
|
||||||
export type MediaKind = "track" | "album" | "artist" | "playlist" | "movie" | "series" | "season" | "episode" | "person" | "channel" | "folder" |
|
export type MediaKind = "track" | "album" | "artist" | "playlist" | "movie" | "series" | "season" | "episode" | "person" |
|
||||||
|
/**
|
||||||
|
* 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
|
* A kind we do not model explicitly. Reached only for provider item types
|
||||||
* that map to nothing meaningful; consumers treat it like an opaque
|
* that map to nothing meaningful; consumers treat it like an opaque
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export type {
|
|||||||
Library,
|
Library,
|
||||||
LiveStreamInfo,
|
LiveStreamInfo,
|
||||||
MediaItem,
|
MediaItem,
|
||||||
|
MediaKind,
|
||||||
MediaSource,
|
MediaSource,
|
||||||
MediaStream,
|
MediaStream,
|
||||||
Person,
|
Person,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. For episodes, try series/season backdrops
|
// 2. For episodes, try series/season backdrops
|
||||||
if (currentItem.type === "Episode") {
|
if (currentItem.kind === "episode") {
|
||||||
if (currentItem.seriesId && currentItem.parentBackdropImageTags?.[0]) {
|
if (currentItem.seriesId && currentItem.parentBackdropImageTags?.[0]) {
|
||||||
return { itemId: currentItem.seriesId, imageType: "Backdrop" as const, tag: currentItem.parentBackdropImageTags[0] };
|
return { itemId: currentItem.seriesId, imageType: "Backdrop" as const, tag: currentItem.parentBackdropImageTags[0] };
|
||||||
}
|
}
|
||||||
@@ -45,17 +45,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. For music tracks, try album backdrop
|
// 3. For music tracks, try album backdrop
|
||||||
if (currentItem.type === "Audio" && currentItem.albumId) {
|
if (currentItem.kind === "track" && currentItem.albumId) {
|
||||||
return { itemId: currentItem.albumId, imageType: "Backdrop" as const, tag: undefined };
|
return { itemId: currentItem.albumId, imageType: "Backdrop" as const, tag: undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Fall back to primary image
|
// 4. Fall back to primary image
|
||||||
if (currentItem.primaryImageTag) {
|
if (currentItem.imageId) {
|
||||||
return { itemId: currentItem.id, imageType: "Primary" as const, tag: currentItem.primaryImageTag };
|
return { itemId: currentItem.id, imageType: "Primary" as const, tag: currentItem.imageId };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Last resort for audio: album primary
|
// 5. Last resort for audio: album primary
|
||||||
if (currentItem.type === "Audio" && currentItem.albumId) {
|
if (currentItem.kind === "track" && currentItem.albumId) {
|
||||||
return { itemId: currentItem.albumId, imageType: "Primary" as const, tag: undefined };
|
return { itemId: currentItem.albumId, imageType: "Primary" as const, tag: undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@
|
|||||||
onclick={() => {
|
onclick={() => {
|
||||||
// Navigate to full series detail page with cast/crew/related content
|
// Navigate to full series detail page with cast/crew/related content
|
||||||
// (even for episodes, show the series page so users see cast and related items)
|
// (even for episodes, show the series page so users see cast and related items)
|
||||||
if (currentItem.type === "Episode" && currentItem.seriesId) {
|
if (currentItem.kind === "episode" && currentItem.seriesId) {
|
||||||
goto(`/library/${currentItem.seriesId}`);
|
goto(`/library/${currentItem.seriesId}`);
|
||||||
} else {
|
} else {
|
||||||
goto(`/library/${currentItem.id}`);
|
goto(`/library/${currentItem.id}`);
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
sortBy: "DateCreated",
|
sortBy: "DateCreated",
|
||||||
sortOrder: "Descending"
|
sortOrder: "Descending"
|
||||||
});
|
});
|
||||||
albums = albumsResult.items.filter(item => item.type === "MusicAlbum");
|
albums = albumsResult.items.filter(item => item.kind === "album");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to load albums:", e);
|
console.warn("Failed to load albums:", e);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
sortBy: "CommunityRating",
|
sortBy: "CommunityRating",
|
||||||
sortOrder: "Descending"
|
sortOrder: "Descending"
|
||||||
});
|
});
|
||||||
topTracks = tracksResult.items.filter(item => item.type === "Audio");
|
topTracks = tracksResult.items.filter(item => item.kind === "track");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to load tracks:", e);
|
console.warn("Failed to load tracks:", e);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
sortOrder: "Descending"
|
sortOrder: "Descending"
|
||||||
});
|
});
|
||||||
relatedArtists = relatedResult.items
|
relatedArtists = relatedResult.items
|
||||||
.filter(item => item.id !== artist.id && item.type === "MusicArtist")
|
.filter(item => item.id !== artist.id && item.kind === "artist")
|
||||||
.slice(0, 6);
|
.slice(0, 6);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,32 +1,35 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
|
|
||||||
|
import type { MediaKind } from "$lib/api/types";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
genres: string[];
|
genres: string[];
|
||||||
maxShow?: number; // Default: unlimited
|
maxShow?: number; // Default: unlimited
|
||||||
clickable?: boolean; // Default: true
|
clickable?: boolean; // Default: true
|
||||||
itemType?: string; // Determines which genre browse page to open
|
itemKind?: MediaKind; // Determines which genre browse page to open
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
genres,
|
genres,
|
||||||
maxShow,
|
maxShow,
|
||||||
clickable = true,
|
clickable = true,
|
||||||
itemType
|
itemKind
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
// Map the item type to its genre-browse route
|
// Map the item kind to its genre-browse route
|
||||||
function genreBasePath(type: string | undefined): string {
|
function genreBasePath(kind: MediaKind | undefined): string {
|
||||||
switch (type) {
|
switch (kind) {
|
||||||
case "MusicAlbum":
|
case "album":
|
||||||
case "MusicArtist":
|
case "artist":
|
||||||
case "Audio":
|
case "track":
|
||||||
|
case "playlist":
|
||||||
return "/library/music/genres";
|
return "/library/music/genres";
|
||||||
case "Series":
|
case "series":
|
||||||
case "Season":
|
case "season":
|
||||||
case "Episode":
|
case "episode":
|
||||||
return "/library/shows/genres";
|
return "/library/shows/genres";
|
||||||
case "Movie":
|
case "movie":
|
||||||
return "/library/movies/genres";
|
return "/library/movies/genres";
|
||||||
default:
|
default:
|
||||||
return "/library/movies/genres";
|
return "/library/movies/genres";
|
||||||
@@ -43,7 +46,7 @@
|
|||||||
|
|
||||||
function handleGenreClick(genre: string) {
|
function handleGenreClick(genre: string) {
|
||||||
if (clickable) {
|
if (clickable) {
|
||||||
goto(`${genreBasePath(itemType)}?genre=${encodeURIComponent(genre)}`);
|
goto(`${genreBasePath(itemKind)}?genre=${encodeURIComponent(genre)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -101,11 +101,11 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isMusicType = $derived(
|
const isMusicType = $derived(
|
||||||
"type" in item && (item.type === "Audio" || item.type === "MusicAlbum" || item.type === "MusicArtist" || item.type === "Playlist")
|
"kind" in item && (item.kind === "track" || item.kind === "album" || item.kind === "artist" || item.kind === "playlist")
|
||||||
);
|
);
|
||||||
|
|
||||||
const aspectRatio = $derived(() => {
|
const aspectRatio = $derived(() => {
|
||||||
if ("type" in item) {
|
if ("kind" in item) {
|
||||||
return isMusicType ? "aspect-square" : "aspect-[2/3]";
|
return isMusicType ? "aspect-square" : "aspect-[2/3]";
|
||||||
}
|
}
|
||||||
// Library
|
// Library
|
||||||
|
|||||||
@@ -31,8 +31,8 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Separate movies and series
|
// Separate movies and series
|
||||||
movies = result.items.filter(item => item.type === "Movie");
|
movies = result.items.filter(item => item.kind === "movie");
|
||||||
series = result.items.filter(item => item.type === "Series");
|
series = result.items.filter(item => item.kind === "series");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load filmography:", e);
|
console.error("Failed to load filmography:", e);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { auth } from "$lib/stores/auth";
|
import { auth } from "$lib/stores/auth";
|
||||||
import type { MediaItem, Person } from "$lib/api/types";
|
import type { MediaItem, MediaKind, Person } from "$lib/api/types";
|
||||||
import MediaCard from "./MediaCard.svelte";
|
import MediaCard from "./MediaCard.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
currentItemId: string;
|
currentItemId: string;
|
||||||
itemType: "Movie" | "Series" | "MusicAlbum" | "Audio";
|
itemKind: MediaKind;
|
||||||
genres?: string[];
|
genres?: string[];
|
||||||
people?: Person[];
|
people?: Person[];
|
||||||
artistIds?: string[];
|
artistIds?: string[];
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
currentItemId,
|
currentItemId,
|
||||||
itemType,
|
itemKind,
|
||||||
genres = [],
|
genres = [],
|
||||||
people = [],
|
people = [],
|
||||||
artistIds = [],
|
artistIds = [],
|
||||||
@@ -45,9 +45,9 @@
|
|||||||
|
|
||||||
let items: MediaItem[] = [];
|
let items: MediaItem[] = [];
|
||||||
|
|
||||||
// First, try to use the Jellyfin Similar Items API (preferred method)
|
// First, try to use the Similar Items API (preferred method)
|
||||||
// This works for Movies and Series (most common cases)
|
// This works for Movies and Series (most common cases)
|
||||||
if (["Movie", "Series"].includes(itemType)) {
|
if (itemKind === "movie" || itemKind === "series") {
|
||||||
try {
|
try {
|
||||||
const result = await repo.getSimilarItems(currentItemId, limit);
|
const result = await repo.getSimilarItems(currentItemId, limit);
|
||||||
items = result.items.filter(item => item.id !== currentItemId);
|
items = result.items.filter(item => item.id !== currentItemId);
|
||||||
@@ -65,10 +65,14 @@
|
|||||||
// Fallback: Load by genres using search (works for all item types)
|
// Fallback: Load by genres using search (works for all item types)
|
||||||
if (genres && genres.length > 0) {
|
if (genres && genres.length > 0) {
|
||||||
try {
|
try {
|
||||||
// Search by first genre to find related items
|
// Search by first genre to find related items. This single-kind query
|
||||||
|
// maps the neutral kind to the concrete Jellyfin item type it needs.
|
||||||
const searchTerm = genres[0];
|
const searchTerm = genres[0];
|
||||||
|
const itemTypeForKind: Record<string, string> = {
|
||||||
|
movie: "Movie", series: "Series", album: "MusicAlbum", track: "Audio", artist: "MusicArtist",
|
||||||
|
};
|
||||||
const result = await repo.search(searchTerm, {
|
const result = await repo.search(searchTerm, {
|
||||||
includeItemTypes: itemType === "MusicAlbum" ? ["MusicAlbum"] : itemType === "Audio" ? ["Audio"] : [itemType],
|
includeItemTypes: [itemTypeForKind[itemKind] ?? "Movie"],
|
||||||
limit: limit * 2
|
limit: limit * 2
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -79,7 +83,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For music albums, also try to load by artist (if we don't have enough from similar API)
|
// For music albums, also try to load by artist (if we don't have enough from similar API)
|
||||||
if (itemType === "MusicAlbum" && artistIds && artistIds.length > 0 && items.length === 0) {
|
if (itemKind === "album" && artistIds && artistIds.length > 0 && items.length === 0) {
|
||||||
try {
|
try {
|
||||||
// Search for other albums by artist name from first artist
|
// Search for other albums by artist name from first artist
|
||||||
const result = await repo.search(artistIds[0], {
|
const result = await repo.search(artistIds[0], {
|
||||||
@@ -109,14 +113,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getTitle(): string {
|
function getTitle(): string {
|
||||||
switch (itemType) {
|
switch (itemKind) {
|
||||||
case "Movie":
|
case "movie":
|
||||||
return "Related Movies";
|
return "Related Movies";
|
||||||
case "Series":
|
case "series":
|
||||||
return "Related Shows";
|
return "Related Shows";
|
||||||
case "MusicAlbum":
|
case "album":
|
||||||
return "Related Albums";
|
return "Related Albums";
|
||||||
case "Audio":
|
case "track":
|
||||||
return "Related Tracks";
|
return "Related Tracks";
|
||||||
default:
|
default:
|
||||||
return "Related Items";
|
return "Related Items";
|
||||||
@@ -133,7 +137,7 @@
|
|||||||
|
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<!-- Skeleton loading state -->
|
<!-- Skeleton loading state -->
|
||||||
{@const isMusicContent = itemType === "MusicAlbum" || itemType === "Audio"}
|
{@const isMusicContent = itemKind === "album" || itemKind === "track"}
|
||||||
<div class="grid grid-cols-2 md:grid-cols-6 gap-4">
|
<div class="grid grid-cols-2 md:grid-cols-6 gap-4">
|
||||||
{#each Array(6) as _}
|
{#each Array(6) as _}
|
||||||
<div class="animate-pulse">
|
<div class="animate-pulse">
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ function createTvStore() {
|
|||||||
|
|
||||||
// Resume items are already video-only from the server, but keep episodes
|
// Resume items are already video-only from the server, but keep episodes
|
||||||
// (and the occasional movie that lives in a mixed library) defensively.
|
// (and the occasional movie that lives in a mixed library) defensively.
|
||||||
const continueWatching = resume.filter(i => i.type === "Episode" || i.type === "Movie");
|
const continueWatching = resume.filter(i => i.kind === "episode" || i.kind === "movie");
|
||||||
|
|
||||||
// Mix the hero: in-progress episodes first (most personal), then next-up,
|
// Mix the hero: in-progress episodes first (most personal), then next-up,
|
||||||
// recent additions, and random series from across the library.
|
// recent additions, and random series from across the library.
|
||||||
|
|||||||
@@ -57,14 +57,13 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function handleItemClick(item: MediaItem) {
|
function handleItemClick(item: MediaItem) {
|
||||||
switch (item.type) {
|
switch (item.kind) {
|
||||||
case "Series":
|
case "series":
|
||||||
case "Season":
|
case "season":
|
||||||
case "MusicAlbum":
|
case "album":
|
||||||
case "MusicArtist":
|
case "artist":
|
||||||
case "Folder":
|
case "folder":
|
||||||
case "Channel":
|
case "channel":
|
||||||
case "ChannelFolderItem":
|
|
||||||
goto(`/library/${item.id}`);
|
goto(`/library/${item.id}`);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -100,7 +99,7 @@
|
|||||||
|
|
||||||
const heroItems = $derived($home.heroItems);
|
const heroItems = $derived($home.heroItems);
|
||||||
const resumeItems = $derived($home.resumeItems.filter(
|
const resumeItems = $derived($home.resumeItems.filter(
|
||||||
i => i.type === "Movie" || i.type === "Episode"
|
i => i.kind === "movie" || i.kind === "episode"
|
||||||
));
|
));
|
||||||
const nextUpItems = $derived($home.nextUpItems);
|
const nextUpItems = $derived($home.nextUpItems);
|
||||||
const latestItems = $derived($home.latestItems);
|
const latestItems = $derived($home.latestItems);
|
||||||
|
|||||||
@@ -127,30 +127,27 @@
|
|||||||
// Prevent accidental taps during scrolling (Android)
|
// Prevent accidental taps during scrolling (Android)
|
||||||
if (scrollGuard.isScrollActive()) return;
|
if (scrollGuard.isScrollActive()) return;
|
||||||
|
|
||||||
if ("type" in item) {
|
if ("kind" in item) {
|
||||||
// It's a MediaItem
|
// It's a MediaItem
|
||||||
const mediaItem = item as MediaItem;
|
const mediaItem = item as MediaItem;
|
||||||
// A ChannelFolderItem can be a folder (drill in) or a playable leaf.
|
// A playable channel leaf plays directly; a channel container drills in.
|
||||||
if (mediaItem.type === "ChannelFolderItem" && !mediaItem.isFolder) {
|
if (mediaItem.kind === "channelItem") {
|
||||||
goto(`/player/${mediaItem.id}`);
|
goto(`/player/${mediaItem.id}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch (mediaItem.type) {
|
switch (mediaItem.kind) {
|
||||||
case "Series":
|
case "series":
|
||||||
case "Movie":
|
case "movie":
|
||||||
case "MusicAlbum":
|
case "album":
|
||||||
case "MusicArtist":
|
case "artist":
|
||||||
case "Folder":
|
case "folder":
|
||||||
case "CollectionFolder":
|
case "playlist":
|
||||||
case "Playlist":
|
case "channel":
|
||||||
case "Channel":
|
|
||||||
case "ChannelFolderItem":
|
|
||||||
// Navigate to detail view
|
// Navigate to detail view
|
||||||
goto(`/library/${mediaItem.id}`);
|
goto(`/library/${mediaItem.id}`);
|
||||||
break;
|
break;
|
||||||
case "Episode":
|
case "episode":
|
||||||
case "TvChannel":
|
// Episodes play directly
|
||||||
// Episodes and live TV channels play directly
|
|
||||||
goto(`/player/${mediaItem.id}`);
|
goto(`/player/${mediaItem.id}`);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -94,7 +94,7 @@
|
|||||||
|
|
||||||
// Set currentLibrary for music items if not already set
|
// Set currentLibrary for music items if not already set
|
||||||
// This ensures navigation to music library pages works correctly
|
// This ensures navigation to music library pages works correctly
|
||||||
if ((item?.type === "MusicAlbum" || item?.type === "MusicArtist" || item?.type === "Audio") && !$currentLibrary) {
|
if ((item?.kind === "album" || item?.kind === "artist" || item?.kind === "track") && !$currentLibrary) {
|
||||||
// Find the music library
|
// Find the music library
|
||||||
if ($libraries.length === 0) {
|
if ($libraries.length === 0) {
|
||||||
await library.loadLibraries();
|
await library.loadLibraries();
|
||||||
@@ -110,8 +110,8 @@
|
|||||||
|
|
||||||
// Ensure cast/crew data is loaded for Movies, Series, and Episodes
|
// Ensure cast/crew data is loaded for Movies, Series, and Episodes
|
||||||
// Some APIs/caches may not include people data on first load
|
// Some APIs/caches may not include people data on first load
|
||||||
if ((item?.type === "Movie" || item?.type === "Series" || item?.type === "Episode") && (!item.people || item.people.length === 0)) {
|
if ((item?.kind === "movie" || item?.kind === "series" || item?.kind === "episode") && (!item.people || item.people.length === 0)) {
|
||||||
console.log(`[LibraryDetail] ⚠ People data missing, reloading ${item?.type}...`);
|
console.log(`[LibraryDetail] ⚠ People data missing, reloading ${item?.kind}...`);
|
||||||
try {
|
try {
|
||||||
const repo = auth.getRepository();
|
const repo = auth.getRepository();
|
||||||
const fullItem = await repo.getItem(itemId);
|
const fullItem = await repo.getItem(itemId);
|
||||||
@@ -124,20 +124,20 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`Could not reload ${item?.type} with full cast data:`, e);
|
console.warn(`Could not reload ${item?.kind} with full cast data:`, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For Series, load seasons and their episodes
|
// For Series, load seasons and their episodes
|
||||||
if (item?.type === "Series") {
|
if (item?.kind === "series") {
|
||||||
const seasons = $libraryItems.filter((i) => i.type === "Season");
|
const seasons = $libraryItems.filter((i) => i.kind === "season");
|
||||||
const repo = auth.getRepository();
|
const repo = auth.getRepository();
|
||||||
|
|
||||||
// Load episodes for each season in parallel
|
// Load episodes for each season in parallel
|
||||||
const seasonDataPromises = seasons.map(async (season) => {
|
const seasonDataPromises = seasons.map(async (season) => {
|
||||||
const result = await repo.getItems(season.id, { limit: 100 });
|
const result = await repo.getItems(season.id, { limit: 100 });
|
||||||
const episodes = result.items
|
const episodes = result.items
|
||||||
.filter((i) => i.type === "Episode")
|
.filter((i) => i.kind === "episode")
|
||||||
.sort((a, b) => (a.indexNumber || 0) - (b.indexNumber || 0));
|
.sort((a, b) => (a.indexNumber || 0) - (b.indexNumber || 0));
|
||||||
return { season, episodes };
|
return { season, episodes };
|
||||||
});
|
});
|
||||||
@@ -190,21 +190,20 @@
|
|||||||
}
|
}
|
||||||
// A ChannelFolderItem can be either a folder (drill in) or a playable leaf.
|
// A ChannelFolderItem can be either a folder (drill in) or a playable leaf.
|
||||||
// Route non-folder channel items straight to the player.
|
// Route non-folder channel items straight to the player.
|
||||||
if (clickedItem.type === "ChannelFolderItem" && !clickedItem.isFolder) {
|
if (clickedItem.kind === "channelItem" || clickedItem.kind === "liveChannel") {
|
||||||
goto(`/player/${clickedItem.id}`);
|
goto(`/player/${clickedItem.id}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch (clickedItem.type) {
|
switch (clickedItem.kind) {
|
||||||
case "Series":
|
case "series":
|
||||||
case "Season":
|
case "season":
|
||||||
case "MusicAlbum":
|
case "album":
|
||||||
case "MusicArtist":
|
case "artist":
|
||||||
case "Folder":
|
case "folder":
|
||||||
case "Playlist":
|
case "playlist":
|
||||||
case "Channel":
|
case "channel":
|
||||||
case "ChannelFolderItem":
|
case "episode":
|
||||||
case "Episode":
|
case "movie":
|
||||||
case "Movie":
|
|
||||||
goto(`/library/${clickedItem.id}`);
|
goto(`/library/${clickedItem.id}`);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -223,9 +222,9 @@
|
|||||||
|
|
||||||
async function handlePlayAll() {
|
async function handlePlayAll() {
|
||||||
// For single items (Episode, Movie), play the item directly
|
// For single items (Episode, Movie), play the item directly
|
||||||
if (item?.type === "Episode" || item?.type === "Movie") {
|
if (item?.kind === "episode" || item?.kind === "movie") {
|
||||||
goto(`/player/${itemId}`);
|
goto(`/player/${itemId}`);
|
||||||
} else if (item?.type === "MusicAlbum" && $libraryItems.length > 0) {
|
} else if (item?.kind === "album" && $libraryItems.length > 0) {
|
||||||
// For albums, use the backend command (backend fetches and queues all tracks)
|
// For albums, use the backend command (backend fetches and queues all tracks)
|
||||||
try {
|
try {
|
||||||
const repo = auth.getRepository();
|
const repo = auth.getRepository();
|
||||||
@@ -248,7 +247,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleShufflePlay() {
|
async function handleShufflePlay() {
|
||||||
if (item?.type === "MusicAlbum" && $libraryItems.length > 0) {
|
if (item?.kind === "album" && $libraryItems.length > 0) {
|
||||||
// For albums, use the backend command with shuffle
|
// For albums, use the backend command with shuffle
|
||||||
try {
|
try {
|
||||||
const repo = auth.getRepository();
|
const repo = auth.getRepository();
|
||||||
@@ -284,7 +283,7 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isMusicItem = $derived(
|
const isMusicItem = $derived(
|
||||||
item?.type === "Audio" || item?.type === "MusicAlbum" || item?.type === "MusicArtist" || item?.type === "Playlist"
|
item?.kind === "track" || item?.kind === "album" || item?.kind === "artist" || item?.kind === "playlist"
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleBackToSeries() {
|
function handleBackToSeries() {
|
||||||
@@ -331,13 +330,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else if item}
|
{:else if item}
|
||||||
<!-- Person Detail View - shown for Person items -->
|
<!-- Person Detail View - shown for Person items -->
|
||||||
{#if item.type === "Person"}
|
{#if item.kind === "person"}
|
||||||
<div class="pt-4">
|
<div class="pt-4">
|
||||||
<BackButton onClick={goBack} label="Back" />
|
<BackButton onClick={goBack} label="Back" />
|
||||||
</div>
|
</div>
|
||||||
<PersonDetailView person={item} />
|
<PersonDetailView person={item} />
|
||||||
<!-- Episode Focus View - shown when navigating with ?episode param -->
|
<!-- Episode Focus View - shown when navigating with ?episode param -->
|
||||||
{:else if item.type === "Series" && focusedEpisode}
|
{:else if item.kind === "series" && focusedEpisode}
|
||||||
<EpisodeFocusView
|
<EpisodeFocusView
|
||||||
episode={focusedEpisode}
|
episode={focusedEpisode}
|
||||||
series={item}
|
series={item}
|
||||||
@@ -381,7 +380,7 @@
|
|||||||
<div class="flex-1 space-y-4">
|
<div class="flex-1 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-bold text-white">{item.name}</h1>
|
<h1 class="text-3xl font-bold text-white">{item.name}</h1>
|
||||||
{#if item.type === "Episode" && (item.parentIndexNumber || item.indexNumber)}
|
{#if item.kind === "episode" && (item.parentIndexNumber || item.indexNumber)}
|
||||||
<p class="text-lg text-gray-400 mt-1">
|
<p class="text-lg text-gray-400 mt-1">
|
||||||
{#if item.parentIndexNumber}Season {item.parentIndexNumber}{/if}
|
{#if item.parentIndexNumber}Season {item.parentIndexNumber}{/if}
|
||||||
{#if item.parentIndexNumber && item.indexNumber}, {/if}
|
{#if item.parentIndexNumber && item.indexNumber}, {/if}
|
||||||
@@ -430,7 +429,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Play
|
Play
|
||||||
</button>
|
</button>
|
||||||
{#if item.type !== "Episode" && item.type !== "Movie"}
|
{#if item.kind !== "episode" && item.kind !== "movie"}
|
||||||
<button
|
<button
|
||||||
onclick={handleShufflePlay}
|
onclick={handleShufflePlay}
|
||||||
class="px-6 py-2 bg-[var(--color-surface)] hover:bg-[var(--color-surface-hover)] rounded-lg font-medium flex items-center gap-2 transition-colors"
|
class="px-6 py-2 bg-[var(--color-surface)] hover:bg-[var(--color-surface-hover)] rounded-lg font-medium flex items-center gap-2 transition-colors"
|
||||||
@@ -441,26 +440,26 @@
|
|||||||
Shuffle
|
Shuffle
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if item.type === "MusicAlbum"}
|
{#if item.kind === "album"}
|
||||||
<AlbumDownloadButton
|
<AlbumDownloadButton
|
||||||
albumId={item.id}
|
albumId={item.id}
|
||||||
albumName={item.name}
|
albumName={item.name}
|
||||||
tracks={$libraryItems}
|
tracks={$libraryItems}
|
||||||
/>
|
/>
|
||||||
{:else if item.type === "Series"}
|
{:else if item.kind === "series"}
|
||||||
<SeriesDownloadButton
|
<SeriesDownloadButton
|
||||||
seriesId={item.id}
|
seriesId={item.id}
|
||||||
seriesName={item.name}
|
seriesName={item.name}
|
||||||
episodeCount={allEpisodes.length || undefined}
|
episodeCount={allEpisodes.length || undefined}
|
||||||
/>
|
/>
|
||||||
{:else if item.type === "Movie"}
|
{:else if item.kind === "movie"}
|
||||||
<VideoDownloadButton
|
<VideoDownloadButton
|
||||||
itemId={item.id}
|
itemId={item.id}
|
||||||
itemName={item.name}
|
itemName={item.name}
|
||||||
isMovie={true}
|
isMovie={true}
|
||||||
size="lg"
|
size="lg"
|
||||||
/>
|
/>
|
||||||
{:else if item.type === "Episode"}
|
{:else if item.kind === "episode"}
|
||||||
<VideoDownloadButton
|
<VideoDownloadButton
|
||||||
itemId={item.id}
|
itemId={item.id}
|
||||||
itemName={item.name}
|
itemName={item.name}
|
||||||
@@ -478,7 +477,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Crew Links - for Movies and Series -->
|
<!-- Crew Links - for Movies and Series -->
|
||||||
{#if item.people && (item.type === "Movie" || item.type === "Series")}
|
{#if item.people && (item.kind === "movie" || item.kind === "series")}
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
{#if item.people.some(p => p.type === "Director")}
|
{#if item.people.some(p => p.type === "Director")}
|
||||||
<CrewLinks
|
<CrewLinks
|
||||||
@@ -512,24 +511,24 @@
|
|||||||
<!-- Genre Tags -->
|
<!-- Genre Tags -->
|
||||||
{#if item.genres?.length}
|
{#if item.genres?.length}
|
||||||
<div>
|
<div>
|
||||||
<GenreTags genres={item.genres ?? undefined} maxShow={6} itemType={item.type} />
|
<GenreTags genres={item.genres ?? undefined} maxShow={6} itemKind={item.kind} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Cast / Related — for Movies and Episodes these sit above the content
|
<!-- Cast / Related — for Movies and Episodes these sit above the content
|
||||||
block; for Series they render *below* the seasons instead, so
|
block; for Series they render *below* the seasons instead, so
|
||||||
continuation content precedes discovery content (UX §5B.4). -->
|
continuation content precedes discovery content (UX §5B.4). -->
|
||||||
{#if item.type !== "Series"}
|
{#if item.kind !== "series"}
|
||||||
<!-- Cast Section - for Movies and Episodes -->
|
<!-- Cast Section - for Movies and Episodes -->
|
||||||
{#if (item.type === "Movie" || item.type === "Episode") && item.people?.length}
|
{#if (item.kind === "movie" || item.kind === "episode") && item.people?.length}
|
||||||
<CastSection people={item.people ?? undefined} />
|
<CastSection people={item.people ?? undefined} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Related Items Section - for Movies -->
|
<!-- Related Items Section - for Movies -->
|
||||||
{#if item.type === "Movie" && (item.genres?.length || item.people?.length)}
|
{#if item.kind === "movie" && (item.genres?.length || item.people?.length)}
|
||||||
<RelatedItemsSection
|
<RelatedItemsSection
|
||||||
currentItemId={item.id}
|
currentItemId={item.id}
|
||||||
itemType={item.type}
|
itemKind={item.kind}
|
||||||
genres={item.genres ?? undefined}
|
genres={item.genres ?? undefined}
|
||||||
people={item.people ?? undefined}
|
people={item.people ?? undefined}
|
||||||
limit={12}
|
limit={12}
|
||||||
@@ -539,7 +538,7 @@
|
|||||||
|
|
||||||
<!-- Content items -->
|
<!-- Content items -->
|
||||||
<div>
|
<div>
|
||||||
{#if item.type === "MusicAlbum"}
|
{#if item.kind === "album"}
|
||||||
<!-- Tracks in list view -->
|
<!-- Tracks in list view -->
|
||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
@@ -558,14 +557,14 @@
|
|||||||
{#if item.genres?.length || item.artistItems?.length}
|
{#if item.genres?.length || item.artistItems?.length}
|
||||||
<RelatedItemsSection
|
<RelatedItemsSection
|
||||||
currentItemId={item.id}
|
currentItemId={item.id}
|
||||||
itemType="MusicAlbum"
|
itemKind="album"
|
||||||
genres={item.genres ?? undefined}
|
genres={item.genres ?? undefined}
|
||||||
artistIds={item.artistItems?.map(a => a.id)}
|
artistIds={item.artistItems?.map(a => a.id)}
|
||||||
limit={12}
|
limit={12}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if item.type === "Series"}
|
{:else if item.kind === "series"}
|
||||||
<!-- Series: Seasons with episodes -->
|
<!-- Series: Seasons with episodes -->
|
||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
{#if $isLibraryLoading}
|
{#if $isLibraryLoading}
|
||||||
@@ -595,17 +594,17 @@
|
|||||||
{#if item.genres?.length || item.people?.length}
|
{#if item.genres?.length || item.people?.length}
|
||||||
<RelatedItemsSection
|
<RelatedItemsSection
|
||||||
currentItemId={item.id}
|
currentItemId={item.id}
|
||||||
itemType={item.type}
|
itemKind={item.kind}
|
||||||
genres={item.genres ?? undefined}
|
genres={item.genres ?? undefined}
|
||||||
people={item.people ?? undefined}
|
people={item.people ?? undefined}
|
||||||
limit={12}
|
limit={12}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if item.type === "MusicArtist"}
|
{:else if item.kind === "artist"}
|
||||||
<!-- Enhanced artist detail view with discography -->
|
<!-- Enhanced artist detail view with discography -->
|
||||||
<ArtistDetailView artist={item} />
|
<ArtistDetailView artist={item} />
|
||||||
{:else if item.type === "Playlist"}
|
{:else if item.kind === "playlist"}
|
||||||
<!-- Playlist detail view with track management -->
|
<!-- Playlist detail view with track management -->
|
||||||
<PlaylistDetailView playlist={item} />
|
<PlaylistDetailView playlist={item} />
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { convertFileSrc } from "@tauri-apps/api/core";
|
import { convertFileSrc } from "@tauri-apps/api/core";
|
||||||
import { commands } from "$lib/api/bindings";
|
import { commands } from "$lib/api/bindings";
|
||||||
import type { PlayQueueRequest } 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 { auth } from "$lib/stores/auth";
|
||||||
import { library } from "$lib/stores/library";
|
import { library } from "$lib/stores/library";
|
||||||
import { queue, currentQueueItem, isShuffle, repeatMode, hasNext as hasNextStore, hasPrevious as hasPreviousStore } from "$lib/stores/queue";
|
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
|
// Only for audio content - video content uses direct loading and shouldn't be affected by audio queue
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const queueItem = $currentQueueItem;
|
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) {
|
if (queueItem && queueItem.id !== currentMedia?.id && !currentIsVideo) {
|
||||||
currentMedia = queueItem;
|
currentMedia = queueItem;
|
||||||
}
|
}
|
||||||
@@ -110,7 +110,8 @@
|
|||||||
// treat it as video when it carries a video media stream.
|
// treat it as video when it carries a video media stream.
|
||||||
function isVideoChannelItem(item: MediaItem): boolean {
|
function isVideoChannelItem(item: MediaItem): boolean {
|
||||||
return (
|
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)
|
(item.mediaStreams?.some((s) => s.type === "Video") ?? false)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -125,14 +126,13 @@
|
|||||||
console.log("loadAndPlay: Loading item", id);
|
console.log("loadAndPlay: Loading item", id);
|
||||||
// Load item details
|
// Load item details
|
||||||
const item = await library.loadItem(id);
|
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;
|
currentMedia = item;
|
||||||
|
|
||||||
// Check if this is a non-playable collection type that should be viewed in library instead
|
// 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"];
|
const collectionKinds: MediaKind[] = ["album", "artist", "series", "season", "folder", "playlist", "channel"];
|
||||||
// A ChannelFolderItem that is itself a folder is a container, not playable.
|
if (item.kind && collectionKinds.includes(item.kind)) {
|
||||||
if (collectionTypes.includes(item.type) || (item.type === "ChannelFolderItem" && item.isFolder)) {
|
console.log("loadAndPlay: Redirecting collection type to library:", item.kind);
|
||||||
console.log("loadAndPlay: Redirecting collection type to library:", item.type);
|
|
||||||
goto(`/library/${id}`);
|
goto(`/library/${id}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -144,8 +144,8 @@
|
|||||||
const alreadyPlayingMedia = get(storeCurrentMedia);
|
const alreadyPlayingMedia = get(storeCurrentMedia);
|
||||||
if (alreadyPlayingMedia?.id === id && !startPosition && !forceRestart) {
|
if (alreadyPlayingMedia?.id === id && !startPosition && !forceRestart) {
|
||||||
console.log("loadAndPlay: Track already playing, showing UI without restarting");
|
console.log("loadAndPlay: Track already playing, showing UI without restarting");
|
||||||
isLive = item.type === "TvChannel";
|
isLive = item.kind === "liveChannel";
|
||||||
isVideo = item.type === "Movie" || item.type === "Episode" || isLive || isVideoChannelItem(item);
|
isVideo = item.kind === "movie" || item.kind === "episode" || isLive || isVideoChannelItem(item);
|
||||||
isPlaying = true;
|
isPlaying = true;
|
||||||
loading = false;
|
loading = false;
|
||||||
// hasNext/hasPrevious come from the event-driven queue store.
|
// 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
|
// Determine if this is video content (Movie, Episode, live TV channels, and
|
||||||
// channel leaf items that carry a video stream).
|
// channel leaf items that carry a video stream).
|
||||||
isLive = item.type === "TvChannel";
|
isLive = item.kind === "liveChannel";
|
||||||
isVideo = item.type === "Movie" || item.type === "Episode" || isLive || isVideoChannelItem(item);
|
isVideo = item.kind === "movie" || item.kind === "episode" || isLive || isVideoChannelItem(item);
|
||||||
|
|
||||||
// When switching to video, stop audio playback and clear the queue
|
// When switching to video, stop audio playback and clear the queue
|
||||||
// This prevents audio from continuing in the background and clears stale state
|
// This prevents audio from continuing in the background and clears stale state
|
||||||
@@ -330,7 +330,7 @@
|
|||||||
sortOrder: "Ascending",
|
sortOrder: "Ascending",
|
||||||
limit: 500,
|
limit: 500,
|
||||||
});
|
});
|
||||||
const audioTracks = result.items.filter(t => t.type === "Audio");
|
const audioTracks = result.items.filter(t => t.kind === "track");
|
||||||
|
|
||||||
if (audioTracks.length > 0) {
|
if (audioTracks.length > 0) {
|
||||||
// Find the index of the current item in the tracks
|
// Find the index of the current item in the tracks
|
||||||
@@ -428,7 +428,7 @@
|
|||||||
loading = false;
|
loading = false;
|
||||||
|
|
||||||
// Fetch next episode for video episodes (for skip button)
|
// 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) {
|
if (isVideo && currentMedia) {
|
||||||
fetchNextEpisode(currentMedia);
|
fetchNextEpisode(currentMedia);
|
||||||
} else {
|
} else {
|
||||||
@@ -566,8 +566,8 @@
|
|||||||
|
|
||||||
async function fetchNextEpisode(media: MediaItem) {
|
async function fetchNextEpisode(media: MediaItem) {
|
||||||
nextEpisode = null;
|
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 });
|
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.type !== "Episode" || !media.seasonId || media.indexNumber == null) {
|
if (media.kind !== "episode" || !media.seasonId || media.indexNumber == null) {
|
||||||
console.log("[NextEpisode] Skipping - not an episode or missing seasonId/indexNumber");
|
console.log("[NextEpisode] Skipping - not an episode or missing seasonId/indexNumber");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -575,7 +575,7 @@
|
|||||||
const repo = auth.getRepository();
|
const repo = auth.getRepository();
|
||||||
// Fetch all episodes in the season sorted by episode number
|
// Fetch all episodes in the season sorted by episode number
|
||||||
const result = await repo.getItems(media.seasonId, { sortBy: "IndexNumber", sortOrder: "Ascending", limit: 500 });
|
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);
|
console.log("[NextEpisode] Season has", episodes.length, "episodes, current index:", media.indexNumber);
|
||||||
|
|
||||||
// Find the episode after the current one by index number
|
// Find the episode after the current one by index number
|
||||||
|
|||||||
Reference in New Issue
Block a user