E frontend reconciliation (2/2): align consumers to generated bindings; svelte-check clean
Traceability Validation / Check Requirement Traces (push) Failing after 21s
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 13m21s
🏗️ Build and Test JellyTau / Build Android APK (push) Failing after 2m14s

Reconcile the ~50 frontend files that consumed the old hand-written types against
the stricter generated bindings (141 -> 0 svelte-check errors):

- Nullable strictness: null-coalesce/guard at consumer sites; widen shared helpers
  (CachedImage.tag, ticksToSeconds, formatDuration, getSessionIcon, session store
  send*/transferToRemote) to accept null.
- Field-name fixes exposed by codegen: userData.played -> isPlayed; remote
  NowPlayingItem uses album (not albumName); ArtistItem id/name.
- player.ts: normalize remote NowPlayingItem -> MediaItem in mergedMedia.
- ArtistItem now serializes camelCase (PascalCase alias retained for Jellyfin
  deserialize); update its serialize test.
- Update downloads.test.ts to the bundled { request } shape; complete the
  sessions.test.ts NowPlayingItem mock.

Frontend: svelte-check 0 errors, vitest 448 passing. Backend: 387 passing.
This commit is contained in:
2026-06-20 20:49:20 +02:00
parent 76c78e2edc
commit 14e9d7e03a
21 changed files with 85 additions and 68 deletions
@@ -28,6 +28,7 @@
continue;
}
const type = person.type;
if (!type) continue;
if (type in groups) {
groups[type].push(person);
} else {
@@ -84,7 +85,7 @@
<!-- Person image -->
<div class="w-24 h-24 rounded-full overflow-hidden bg-[var(--color-surface)] mb-2">
<CachedImage
itemId={person.id}
itemId={person.id ?? ""}
imageType="Primary"
tag={person.primaryImageTag}
maxWidth={200}
+1 -1
View File
@@ -55,7 +55,7 @@
<div class="flex flex-wrap gap-1">
{#each filteredPeople as person, index (person.id)}
<button
onclick={(e) => handlePersonClick(person.id, e)}
onclick={(e) => handlePersonClick(person.id ?? "", e)}
class="text-[var(--color-jellyfin)] hover:underline"
>
{person.name}
@@ -84,7 +84,7 @@
return null;
});
function formatDuration(ticks?: number): string {
function formatDuration(ticks?: number | null): string {
if (!ticks) return "";
const seconds = Math.floor(ticks / 10000000);
const hours = Math.floor(seconds / 3600);
@@ -100,7 +100,7 @@
if (!ep.userData || !ep.runTimeTicks) {
return 0;
}
return (ep.userData.playbackPositionTicks / ep.runTimeTicks) * 100;
return ((ep.userData.playbackPositionTicks ?? 0) / ep.runTimeTicks) * 100;
}
function handlePlay() {
@@ -178,7 +178,7 @@
{episode.communityRating.toFixed(1)}
</span>
{/if}
{#if episode.userData?.played}
{#if episode.userData?.isPlayed}
<span class="flex items-center gap-1 text-[var(--color-jellyfin)]">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
@@ -281,7 +281,7 @@
{/if}
<!-- Played indicator -->
{#if ep.userData?.played}
{#if ep.userData?.isPlayed}
<div class="absolute top-2 right-2">
<svg class="w-5 h-5 text-[var(--color-jellyfin)]" fill="currentColor" viewBox="0 0 24 24">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
+6 -6
View File
@@ -40,7 +40,7 @@
if (!episode.userData || !episode.runTimeTicks) {
return 0;
}
return (episode.userData.playbackPositionTicks / episode.runTimeTicks) * 100;
return ((episode.userData.playbackPositionTicks ?? 0) / episode.runTimeTicks) * 100;
});
const duration = $derived(formatDuration(episode.runTimeTicks));
@@ -137,7 +137,7 @@
{episode.name}
</h3>
<!-- Played indicator -->
{#if episode.userData?.played}
{#if episode.userData?.isPlayed}
<svg class="w-4 h-4 flex-shrink-0 text-[var(--color-jellyfin)]" fill="currentColor" viewBox="0 0 24 24">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg>
@@ -164,10 +164,10 @@
<VideoDownloadButton
itemId={episode.id}
itemName={episode.name}
seriesName={episode.seriesName}
seasonName={episode.seasonName}
episodeNumber={episode.indexNumber}
seasonNumber={episode.parentIndexNumber}
seriesName={episode.seriesName ?? undefined}
seasonName={episode.seasonName ?? undefined}
episodeNumber={episode.indexNumber ?? undefined}
seasonNumber={episode.parentIndexNumber ?? undefined}
size="sm"
/>
</div>
@@ -18,7 +18,7 @@
}
function getImageTag(item: MediaItem | Library): string | undefined {
return "primaryImageTag" in item ? item.primaryImageTag : ("imageTag" in item ? item.imageTag : undefined);
return "primaryImageTag" in item ? (item.primaryImageTag ?? undefined) : ("imageTag" in item ? (item.imageTag ?? undefined) : undefined);
}
function getSubtitle(item: MediaItem | Library): string {
@@ -44,7 +44,7 @@
if (!showProgress || !("userData" in item) || !item.userData || !("runTimeTicks" in item) || !item.runTimeTicks) {
return 0;
}
return (item.userData.playbackPositionTicks / item.runTimeTicks) * 100;
return ((item.userData.playbackPositionTicks ?? 0) / item.runTimeTicks) * 100;
}
function getTrackNumber(item: MediaItem | Library): string {
@@ -61,7 +61,7 @@
{@const duration = "runTimeTicks" in item ? formatDuration(item.runTimeTicks) : ""}
{@const progress = getProgress(item)}
{@const trackNum = getTrackNumber(item)}
{@const isPlayed = "userData" in item && item.userData?.played}
{@const isPlayed = "userData" in item && item.userData?.isPlayed}
{@const downloadInfo = getDownloadInfo(item.id)}
{@const isDownloaded = downloadInfo?.status === "completed"}
{@const isDownloading = downloadInfo?.status === "downloading" || downloadInfo?.status === "pending"}
+2 -2
View File
@@ -52,7 +52,7 @@
if (!showProgress || !("userData" in item) || !item.userData || !item.runTimeTicks) {
return 0;
}
return (item.userData.playbackPositionTicks / item.runTimeTicks) * 100;
return ((item.userData.playbackPositionTicks ?? 0) / item.runTimeTicks) * 100;
});
const subtitle = $derived(() => {
@@ -112,7 +112,7 @@
{/if}
<!-- Played indicator -->
{#if "userData" in item && item.userData?.played}
{#if "userData" in item && item.userData?.isPlayed}
<div class="absolute top-2 right-2">
<svg class="w-5 h-5 text-[var(--color-jellyfin)]" fill="currentColor" viewBox="0 0 24 24">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
+1 -1
View File
@@ -145,7 +145,7 @@
goto(`/library/${artistId}`);
}
function handleAlbumClick(albumId: string | undefined, e: Event) {
function handleAlbumClick(albumId: string | null | undefined, e: Event) {
if (!albumId) return;
e.stopPropagation();
goto(`/library/${albumId}`);