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:
@@ -43,7 +43,7 @@
|
||||
sortBy: "DateCreated",
|
||||
sortOrder: "Descending"
|
||||
});
|
||||
albums = albumsResult.items.filter(item => item.type === "MusicAlbum");
|
||||
albums = albumsResult.items.filter(item => item.kind === "album");
|
||||
} catch (e) {
|
||||
console.warn("Failed to load albums:", e);
|
||||
} finally {
|
||||
@@ -58,7 +58,7 @@
|
||||
sortBy: "CommunityRating",
|
||||
sortOrder: "Descending"
|
||||
});
|
||||
topTracks = tracksResult.items.filter(item => item.type === "Audio");
|
||||
topTracks = tracksResult.items.filter(item => item.kind === "track");
|
||||
} catch (e) {
|
||||
console.warn("Failed to load tracks:", e);
|
||||
} finally {
|
||||
@@ -76,7 +76,7 @@
|
||||
sortOrder: "Descending"
|
||||
});
|
||||
relatedArtists = relatedResult.items
|
||||
.filter(item => item.id !== artist.id && item.type === "MusicArtist")
|
||||
.filter(item => item.id !== artist.id && item.kind === "artist")
|
||||
.slice(0, 6);
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
import type { MediaKind } from "$lib/api/types";
|
||||
|
||||
interface Props {
|
||||
genres: string[];
|
||||
maxShow?: number; // Default: unlimited
|
||||
clickable?: boolean; // Default: true
|
||||
itemType?: string; // Determines which genre browse page to open
|
||||
itemKind?: MediaKind; // Determines which genre browse page to open
|
||||
}
|
||||
|
||||
let {
|
||||
genres,
|
||||
maxShow,
|
||||
clickable = true,
|
||||
itemType
|
||||
itemKind
|
||||
}: Props = $props();
|
||||
|
||||
// Map the item type to its genre-browse route
|
||||
function genreBasePath(type: string | undefined): string {
|
||||
switch (type) {
|
||||
case "MusicAlbum":
|
||||
case "MusicArtist":
|
||||
case "Audio":
|
||||
// Map the item kind to its genre-browse route
|
||||
function genreBasePath(kind: MediaKind | undefined): string {
|
||||
switch (kind) {
|
||||
case "album":
|
||||
case "artist":
|
||||
case "track":
|
||||
case "playlist":
|
||||
return "/library/music/genres";
|
||||
case "Series":
|
||||
case "Season":
|
||||
case "Episode":
|
||||
case "series":
|
||||
case "season":
|
||||
case "episode":
|
||||
return "/library/shows/genres";
|
||||
case "Movie":
|
||||
case "movie":
|
||||
return "/library/movies/genres";
|
||||
default:
|
||||
return "/library/movies/genres";
|
||||
@@ -43,7 +46,7 @@
|
||||
|
||||
function handleGenreClick(genre: string) {
|
||||
if (clickable) {
|
||||
goto(`${genreBasePath(itemType)}?genre=${encodeURIComponent(genre)}`);
|
||||
goto(`${genreBasePath(itemKind)}?genre=${encodeURIComponent(genre)}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -101,11 +101,11 @@
|
||||
};
|
||||
|
||||
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(() => {
|
||||
if ("type" in item) {
|
||||
if ("kind" in item) {
|
||||
return isMusicType ? "aspect-square" : "aspect-[2/3]";
|
||||
}
|
||||
// Library
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
});
|
||||
|
||||
// Separate movies and series
|
||||
movies = result.items.filter(item => item.type === "Movie");
|
||||
series = result.items.filter(item => item.type === "Series");
|
||||
movies = result.items.filter(item => item.kind === "movie");
|
||||
series = result.items.filter(item => item.kind === "series");
|
||||
} catch (e) {
|
||||
console.error("Failed to load filmography:", e);
|
||||
} finally {
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
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";
|
||||
|
||||
interface Props {
|
||||
currentItemId: string;
|
||||
itemType: "Movie" | "Series" | "MusicAlbum" | "Audio";
|
||||
itemKind: MediaKind;
|
||||
genres?: string[];
|
||||
people?: Person[];
|
||||
artistIds?: string[];
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
let {
|
||||
currentItemId,
|
||||
itemType,
|
||||
itemKind,
|
||||
genres = [],
|
||||
people = [],
|
||||
artistIds = [],
|
||||
@@ -45,9 +45,9 @@
|
||||
|
||||
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)
|
||||
if (["Movie", "Series"].includes(itemType)) {
|
||||
if (itemKind === "movie" || itemKind === "series") {
|
||||
try {
|
||||
const result = await repo.getSimilarItems(currentItemId, limit);
|
||||
items = result.items.filter(item => item.id !== currentItemId);
|
||||
@@ -65,10 +65,14 @@
|
||||
// Fallback: Load by genres using search (works for all item types)
|
||||
if (genres && genres.length > 0) {
|
||||
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 itemTypeForKind: Record<string, string> = {
|
||||
movie: "Movie", series: "Series", album: "MusicAlbum", track: "Audio", artist: "MusicArtist",
|
||||
};
|
||||
const result = await repo.search(searchTerm, {
|
||||
includeItemTypes: itemType === "MusicAlbum" ? ["MusicAlbum"] : itemType === "Audio" ? ["Audio"] : [itemType],
|
||||
includeItemTypes: [itemTypeForKind[itemKind] ?? "Movie"],
|
||||
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)
|
||||
if (itemType === "MusicAlbum" && artistIds && artistIds.length > 0 && items.length === 0) {
|
||||
if (itemKind === "album" && artistIds && artistIds.length > 0 && items.length === 0) {
|
||||
try {
|
||||
// Search for other albums by artist name from first artist
|
||||
const result = await repo.search(artistIds[0], {
|
||||
@@ -109,14 +113,14 @@
|
||||
}
|
||||
|
||||
function getTitle(): string {
|
||||
switch (itemType) {
|
||||
case "Movie":
|
||||
switch (itemKind) {
|
||||
case "movie":
|
||||
return "Related Movies";
|
||||
case "Series":
|
||||
case "series":
|
||||
return "Related Shows";
|
||||
case "MusicAlbum":
|
||||
case "album":
|
||||
return "Related Albums";
|
||||
case "Audio":
|
||||
case "track":
|
||||
return "Related Tracks";
|
||||
default:
|
||||
return "Related Items";
|
||||
@@ -133,7 +137,7 @@
|
||||
|
||||
{#if loading}
|
||||
<!-- 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">
|
||||
{#each Array(6) as _}
|
||||
<div class="animate-pulse">
|
||||
|
||||
Reference in New Issue
Block a user