Merge branch 'frontend-domain-model' into ci-docs-publish-fix
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 8m34s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 5m20s
Traceability Validation / Check Requirement Traces (push) Successful in 15s
Build & Release / Run Tests (push) Successful in 5m18s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 4m38s
Build & Release / Build Linux (push) Successful in 17m22s
Build & Release / Build Android (push) Successful in 22m44s
Build & Release / Create Release (push) Successful in 14s
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 8m34s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 5m20s
Traceability Validation / Check Requirement Traces (push) Successful in 15s
Build & Release / Run Tests (push) Successful in 5m18s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 4m38s
Build & Release / Build Linux (push) Successful in 17m22s
Build & Release / Build Android (push) Successful in 22m44s
Build & Release / Create Release (push) Successful in 14s
This commit is contained in:
@@ -57,14 +57,13 @@
|
||||
});
|
||||
|
||||
function handleItemClick(item: MediaItem) {
|
||||
switch (item.type) {
|
||||
case "Series":
|
||||
case "Season":
|
||||
case "MusicAlbum":
|
||||
case "MusicArtist":
|
||||
case "Folder":
|
||||
case "Channel":
|
||||
case "ChannelFolderItem":
|
||||
switch (item.kind) {
|
||||
case "series":
|
||||
case "season":
|
||||
case "album":
|
||||
case "artist":
|
||||
case "folder":
|
||||
case "channel":
|
||||
goto(`/library/${item.id}`);
|
||||
break;
|
||||
default:
|
||||
@@ -100,7 +99,7 @@
|
||||
|
||||
const heroItems = $derived($home.heroItems);
|
||||
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 latestItems = $derived($home.latestItems);
|
||||
|
||||
@@ -127,30 +127,27 @@
|
||||
// Prevent accidental taps during scrolling (Android)
|
||||
if (scrollGuard.isScrollActive()) return;
|
||||
|
||||
if ("type" in item) {
|
||||
if ("kind" in item) {
|
||||
// It's a MediaItem
|
||||
const mediaItem = item as MediaItem;
|
||||
// A ChannelFolderItem can be a folder (drill in) or a playable leaf.
|
||||
if (mediaItem.type === "ChannelFolderItem" && !mediaItem.isFolder) {
|
||||
// A playable channel leaf plays directly; a channel container drills in.
|
||||
if (mediaItem.kind === "channelItem") {
|
||||
goto(`/player/${mediaItem.id}`);
|
||||
return;
|
||||
}
|
||||
switch (mediaItem.type) {
|
||||
case "Series":
|
||||
case "Movie":
|
||||
case "MusicAlbum":
|
||||
case "MusicArtist":
|
||||
case "Folder":
|
||||
case "CollectionFolder":
|
||||
case "Playlist":
|
||||
case "Channel":
|
||||
case "ChannelFolderItem":
|
||||
switch (mediaItem.kind) {
|
||||
case "series":
|
||||
case "movie":
|
||||
case "album":
|
||||
case "artist":
|
||||
case "folder":
|
||||
case "playlist":
|
||||
case "channel":
|
||||
// Navigate to detail view
|
||||
goto(`/library/${mediaItem.id}`);
|
||||
break;
|
||||
case "Episode":
|
||||
case "TvChannel":
|
||||
// Episodes and live TV channels play directly
|
||||
case "episode":
|
||||
// Episodes play directly
|
||||
goto(`/player/${mediaItem.id}`);
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { page } from "$app/stores";
|
||||
import { goto } from "$app/navigation";
|
||||
import { navigateBack } from "$lib/utils/navigation";
|
||||
import { kindLabel } from "$lib/utils/mediaKind";
|
||||
import { commands } from "$lib/api/bindings";
|
||||
import type { MediaItem, Library } from "$lib/api/types";
|
||||
import { library, libraryItems, isLibraryLoading, currentLibrary, libraries } from "$lib/stores/library";
|
||||
@@ -84,7 +85,7 @@
|
||||
|
||||
try {
|
||||
item = await library.loadItem(itemId);
|
||||
console.log(`[LibraryDetail] ✓ Loaded item: ${item?.name} (${item?.type})`);
|
||||
console.log(`[LibraryDetail] ✓ Loaded item: ${item?.name} (${item?.kind})`);
|
||||
console.log(`[LibraryDetail] - Has people? ${item?.people ? `YES (${item.people.length})` : 'NO'}`);
|
||||
if (item?.people) {
|
||||
item.people.forEach((p, i) => {
|
||||
@@ -94,7 +95,7 @@
|
||||
|
||||
// Set currentLibrary for music items if not already set
|
||||
// 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
|
||||
if ($libraries.length === 0) {
|
||||
await library.loadLibraries();
|
||||
@@ -110,8 +111,8 @@
|
||||
|
||||
// Ensure cast/crew data is loaded for Movies, Series, and Episodes
|
||||
// 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)) {
|
||||
console.log(`[LibraryDetail] ⚠ People data missing, reloading ${item?.type}...`);
|
||||
if ((item?.kind === "movie" || item?.kind === "series" || item?.kind === "episode") && (!item.people || item.people.length === 0)) {
|
||||
console.log(`[LibraryDetail] ⚠ People data missing, reloading ${item?.kind}...`);
|
||||
try {
|
||||
const repo = auth.getRepository();
|
||||
const fullItem = await repo.getItem(itemId);
|
||||
@@ -124,20 +125,20 @@
|
||||
});
|
||||
}
|
||||
} 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
|
||||
if (item?.type === "Series") {
|
||||
const seasons = $libraryItems.filter((i) => i.type === "Season");
|
||||
if (item?.kind === "series") {
|
||||
const seasons = $libraryItems.filter((i) => i.kind === "season");
|
||||
const repo = auth.getRepository();
|
||||
|
||||
// Load episodes for each season in parallel
|
||||
const seasonDataPromises = seasons.map(async (season) => {
|
||||
const result = await repo.getItems(season.id, { limit: 100 });
|
||||
const episodes = result.items
|
||||
.filter((i) => i.type === "Episode")
|
||||
.filter((i) => i.kind === "episode")
|
||||
.sort((a, b) => (a.indexNumber || 0) - (b.indexNumber || 0));
|
||||
return { season, episodes };
|
||||
});
|
||||
@@ -170,9 +171,9 @@
|
||||
|
||||
// Images now handled by CachedImage component
|
||||
|
||||
function formatDuration(ticks?: number): string {
|
||||
if (!ticks) return "";
|
||||
const seconds = Math.floor(ticks / 10000000);
|
||||
function formatDuration(ms?: number | null): string {
|
||||
if (!ms) return "";
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
@@ -183,28 +184,27 @@
|
||||
}
|
||||
|
||||
function handleItemClick(clickedItem: MediaItem | Library) {
|
||||
if (!("type" in clickedItem)) {
|
||||
if (!("kind" in clickedItem)) {
|
||||
// Library item - navigate to library
|
||||
goto(`/library/${clickedItem.id}`);
|
||||
return;
|
||||
}
|
||||
// A ChannelFolderItem can be either a folder (drill in) or a playable leaf.
|
||||
// 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}`);
|
||||
return;
|
||||
}
|
||||
switch (clickedItem.type) {
|
||||
case "Series":
|
||||
case "Season":
|
||||
case "MusicAlbum":
|
||||
case "MusicArtist":
|
||||
case "Folder":
|
||||
case "Playlist":
|
||||
case "Channel":
|
||||
case "ChannelFolderItem":
|
||||
case "Episode":
|
||||
case "Movie":
|
||||
switch (clickedItem.kind) {
|
||||
case "series":
|
||||
case "season":
|
||||
case "album":
|
||||
case "artist":
|
||||
case "folder":
|
||||
case "playlist":
|
||||
case "channel":
|
||||
case "episode":
|
||||
case "movie":
|
||||
goto(`/library/${clickedItem.id}`);
|
||||
break;
|
||||
default:
|
||||
@@ -223,9 +223,9 @@
|
||||
|
||||
async function handlePlayAll() {
|
||||
// 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}`);
|
||||
} 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)
|
||||
try {
|
||||
const repo = auth.getRepository();
|
||||
@@ -248,7 +248,7 @@
|
||||
}
|
||||
|
||||
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
|
||||
try {
|
||||
const repo = auth.getRepository();
|
||||
@@ -284,7 +284,7 @@
|
||||
);
|
||||
|
||||
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() {
|
||||
@@ -331,13 +331,13 @@
|
||||
</div>
|
||||
{:else if item}
|
||||
<!-- Person Detail View - shown for Person items -->
|
||||
{#if item.type === "Person"}
|
||||
{#if item.kind === "person"}
|
||||
<div class="pt-4">
|
||||
<BackButton onClick={goBack} label="Back" />
|
||||
</div>
|
||||
<PersonDetailView person={item} />
|
||||
<!-- Episode Focus View - shown when navigating with ?episode param -->
|
||||
{:else if item.type === "Series" && focusedEpisode}
|
||||
{:else if item.kind === "series" && focusedEpisode}
|
||||
<EpisodeFocusView
|
||||
episode={focusedEpisode}
|
||||
series={item}
|
||||
@@ -355,11 +355,11 @@
|
||||
<div class="flex gap-6">
|
||||
<!-- Poster -->
|
||||
<div class="flex-shrink-0 w-48">
|
||||
{#if item.primaryImageTag}
|
||||
{#if item.imageId}
|
||||
<CachedImage
|
||||
itemId={item.id}
|
||||
imageType="Primary"
|
||||
tag={item.primaryImageTag}
|
||||
tag={item.imageId}
|
||||
maxWidth={400}
|
||||
alt={item.name}
|
||||
class="w-full {isMusicItem ? 'aspect-square' : ''} rounded-lg shadow-lg"
|
||||
@@ -381,7 +381,7 @@
|
||||
<div class="flex-1 space-y-4">
|
||||
<div>
|
||||
<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">
|
||||
{#if item.parentIndexNumber}Season {item.parentIndexNumber}{/if}
|
||||
{#if item.parentIndexNumber && item.indexNumber}, {/if}
|
||||
@@ -403,11 +403,11 @@
|
||||
|
||||
<!-- Metadata -->
|
||||
<div class="flex items-center gap-4 text-sm text-gray-400">
|
||||
{#if item.type}
|
||||
<span class="px-2 py-1 bg-[var(--color-surface)] rounded">{item.type}</span>
|
||||
{#if kindLabel(item.kind)}
|
||||
<span class="px-2 py-1 bg-[var(--color-surface)] rounded">{kindLabel(item.kind)}</span>
|
||||
{/if}
|
||||
{#if item.runTimeTicks}
|
||||
<span>{formatDuration(item.runTimeTicks)}</span>
|
||||
{#if item.durationMs}
|
||||
<span>{formatDuration(item.durationMs)}</span>
|
||||
{/if}
|
||||
{#if item.communityRating}
|
||||
<span class="flex items-center gap-1">
|
||||
@@ -430,7 +430,7 @@
|
||||
</svg>
|
||||
Play
|
||||
</button>
|
||||
{#if item.type !== "Episode" && item.type !== "Movie"}
|
||||
{#if item.kind !== "episode" && item.kind !== "movie"}
|
||||
<button
|
||||
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"
|
||||
@@ -441,26 +441,26 @@
|
||||
Shuffle
|
||||
</button>
|
||||
{/if}
|
||||
{#if item.type === "MusicAlbum"}
|
||||
{#if item.kind === "album"}
|
||||
<AlbumDownloadButton
|
||||
albumId={item.id}
|
||||
albumName={item.name}
|
||||
tracks={$libraryItems}
|
||||
/>
|
||||
{:else if item.type === "Series"}
|
||||
{:else if item.kind === "series"}
|
||||
<SeriesDownloadButton
|
||||
seriesId={item.id}
|
||||
seriesName={item.name}
|
||||
episodeCount={allEpisodes.length || undefined}
|
||||
/>
|
||||
{:else if item.type === "Movie"}
|
||||
{:else if item.kind === "movie"}
|
||||
<VideoDownloadButton
|
||||
itemId={item.id}
|
||||
itemName={item.name}
|
||||
isMovie={true}
|
||||
size="lg"
|
||||
/>
|
||||
{:else if item.type === "Episode"}
|
||||
{:else if item.kind === "episode"}
|
||||
<VideoDownloadButton
|
||||
itemId={item.id}
|
||||
itemName={item.name}
|
||||
@@ -478,7 +478,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
{#if item.people.some(p => p.type === "Director")}
|
||||
<CrewLinks
|
||||
@@ -512,24 +512,24 @@
|
||||
<!-- Genre Tags -->
|
||||
{#if item.genres?.length}
|
||||
<div>
|
||||
<GenreTags genres={item.genres ?? undefined} maxShow={6} itemType={item.type} />
|
||||
<GenreTags genres={item.genres ?? undefined} maxShow={6} itemKind={item.kind} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Cast / Related — for Movies and Episodes these sit above the content
|
||||
block; for Series they render *below* the seasons instead, so
|
||||
continuation content precedes discovery content (UX §5B.4). -->
|
||||
{#if item.type !== "Series"}
|
||||
{#if item.kind !== "series"}
|
||||
<!-- 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} />
|
||||
{/if}
|
||||
|
||||
<!-- 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
|
||||
currentItemId={item.id}
|
||||
itemType={item.type}
|
||||
itemKind={item.kind}
|
||||
genres={item.genres ?? undefined}
|
||||
people={item.people ?? undefined}
|
||||
limit={12}
|
||||
@@ -539,7 +539,7 @@
|
||||
|
||||
<!-- Content items -->
|
||||
<div>
|
||||
{#if item.type === "MusicAlbum"}
|
||||
{#if item.kind === "album"}
|
||||
<!-- Tracks in list view -->
|
||||
<div class="space-y-8">
|
||||
<div class="space-y-4">
|
||||
@@ -558,14 +558,14 @@
|
||||
{#if item.genres?.length || item.artistItems?.length}
|
||||
<RelatedItemsSection
|
||||
currentItemId={item.id}
|
||||
itemType="MusicAlbum"
|
||||
itemKind="album"
|
||||
genres={item.genres ?? undefined}
|
||||
artistIds={item.artistItems?.map(a => a.id)}
|
||||
limit={12}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if item.type === "Series"}
|
||||
{:else if item.kind === "series"}
|
||||
<!-- Series: Seasons with episodes -->
|
||||
<div class="space-y-8">
|
||||
{#if $isLibraryLoading}
|
||||
@@ -595,17 +595,17 @@
|
||||
{#if item.genres?.length || item.people?.length}
|
||||
<RelatedItemsSection
|
||||
currentItemId={item.id}
|
||||
itemType={item.type}
|
||||
itemKind={item.kind}
|
||||
genres={item.genres ?? undefined}
|
||||
people={item.people ?? undefined}
|
||||
limit={12}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if item.type === "MusicArtist"}
|
||||
{:else if item.kind === "artist"}
|
||||
<!-- Enhanced artist detail view with discography -->
|
||||
<ArtistDetailView artist={item} />
|
||||
{:else if item.type === "Playlist"}
|
||||
{:else if item.kind === "playlist"}
|
||||
<!-- Playlist detail view with track management -->
|
||||
<PlaylistDetailView playlist={item} />
|
||||
{:else}
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
});
|
||||
|
||||
function handleItemClick(item: MediaItem) {
|
||||
if (item.type === "Folder") {
|
||||
if (item.kind === "folder") {
|
||||
goto(`/library/${item.id}`);
|
||||
} else {
|
||||
// Movies play directly.
|
||||
|
||||
@@ -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,8 +110,8 @@
|
||||
// treat it as video when it carries a video media stream.
|
||||
function isVideoChannelItem(item: MediaItem): boolean {
|
||||
return (
|
||||
item.type === "ChannelFolderItem" &&
|
||||
(item.mediaStreams?.some((s) => s.type === "Video") ?? false)
|
||||
item.kind === "channelItem" &&
|
||||
(item.mediaStreams?.some((s) => s.kind === "video") ?? false)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -125,14 +125,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 +143,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 +157,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
|
||||
@@ -185,9 +184,9 @@
|
||||
const progress = await commands.storageGetPlaybackProgress(userId, id);
|
||||
console.log("Resume check - retrieved progress:", progress);
|
||||
|
||||
if (progress && progress.positionTicks > 0 && item.runTimeTicks) {
|
||||
const positionSeconds = progress.positionTicks / 10_000_000;
|
||||
const totalSeconds = item.runTimeTicks / 10_000_000;
|
||||
if (progress && progress.positionMs > 0 && item.durationMs) {
|
||||
const positionSeconds = progress.positionMs / 1000;
|
||||
const totalSeconds = item.durationMs / 1000;
|
||||
const progressPercent = (positionSeconds / totalSeconds) * 100;
|
||||
|
||||
console.log("Resume check - positionSeconds:", positionSeconds, "totalSeconds:", totalSeconds, "progressPercent:", progressPercent);
|
||||
@@ -206,7 +205,7 @@
|
||||
console.log("Resume check - NOT showing dialog. Position > 30?", positionSeconds > 30, "Progress < 90?", progressPercent < 90);
|
||||
}
|
||||
} else {
|
||||
console.log("Resume check - No valid progress found. Has progress?", !!progress, "Has position?", progress?.positionTicks, "Has runtime?", !!item.runTimeTicks);
|
||||
console.log("Resume check - No valid progress found. Has progress?", !!progress, "Has position?", progress?.positionMs, "Has runtime?", !!item.durationMs);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to check saved progress:", e);
|
||||
@@ -330,7 +329,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
|
||||
@@ -354,9 +353,9 @@
|
||||
title: t.name,
|
||||
artist: t.artists?.join(", ") || null,
|
||||
album: t.albumName || null,
|
||||
duration: t.runTimeTicks ? t.runTimeTicks / 10000000 : null,
|
||||
artworkUrl: t.primaryImageTag
|
||||
? repo.getImageUrl(t.albumId || t.id, "Primary", { maxWidth: 300, tag: t.primaryImageTag })
|
||||
duration: t.durationMs ? t.durationMs / 1000 : null,
|
||||
artworkUrl: t.imageId
|
||||
? repo.getImageUrl(t.albumId || t.id, "Primary", { maxWidth: 300, tag: t.imageId })
|
||||
: null,
|
||||
mediaType: "audio",
|
||||
streamUrl: trackStreamUrl,
|
||||
@@ -428,7 +427,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 +565,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 +574,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