Fixes for tests
🏗️ Build and Test JellyTau / Run Tests (push) Failing after 31s
🏗️ Build and Test JellyTau / Build Android APK (push) Has been skipped
Traceability Validation / Check Requirement Traces (push) Failing after 3s

This commit is contained in:
2026-06-20 15:28:15 +02:00
parent d6aa74fc85
commit c959c07ab4
13 changed files with 163 additions and 110 deletions
+8 -3
View File
@@ -26,6 +26,7 @@
let imageUrl = $state<string | null>(null);
let loading = $state(true);
let error = $state(false);
let lastLoadKey = "";
async function loadImage() {
if (!itemId) {
@@ -68,10 +69,14 @@
}
}
// Reload image when props change
// Only reload when the image identity changes (not on parent re-renders)
$effect(() => {
imageUrl = null;
loadImage();
const loadKey = `${itemId}|${imageType}|${tag || ""}`;
if (loadKey !== lastLoadKey) {
lastLoadKey = loadKey;
imageUrl = null;
loadImage();
}
});
</script>
@@ -69,7 +69,8 @@
}
try {
loading = true;
// Only show skeleton on first load (no data yet)
if (items.length === 0) loading = true;
const repo = auth.getRepository();
// Use backend search if search query is provided, otherwise use getItems with sort
@@ -192,7 +193,7 @@
</div>
{:else}
{#if config.displayComponent === "grid"}
<LibraryGrid items={items} onItemClick={handleItemClick} />
<LibraryGrid items={items} onItemClick={handleItemClick} musicContent={["MusicAlbum", "MusicArtist", "Audio", "Playlist"].includes(config.itemType)} />
{:else if config.displayComponent === "tracklist"}
<TrackList tracks={items} onTrackClick={handleTrackClick} />
{/if}
@@ -10,10 +10,11 @@
loading?: boolean;
showViewToggle?: boolean;
forceGrid?: boolean;
musicContent?: boolean;
onItemClick?: (item: MediaItem | Library) => void;
}
let { items, title, loading = false, showViewToggle = true, forceGrid = false, onItemClick }: Props = $props();
let { items, title, loading = false, showViewToggle = true, forceGrid = false, musicContent = false, onItemClick }: Props = $props();
</script>
<div class="space-y-4">
@@ -54,7 +55,7 @@
<div class="flex gap-4 overflow-hidden">
{#each Array(6) as _}
<div class="w-36 flex-shrink-0 animate-pulse">
<div class="aspect-[2/3] bg-[var(--color-surface)] rounded-lg"></div>
<div class="{musicContent ? 'aspect-square' : 'aspect-[2/3]'} bg-[var(--color-surface)] rounded-lg"></div>
<div class="mt-2 h-4 bg-[var(--color-surface)] rounded w-3/4"></div>
<div class="mt-1 h-3 bg-[var(--color-surface)] rounded w-1/2"></div>
</div>
+6 -3
View File
@@ -30,13 +30,16 @@
large: "w-48",
};
const isMusicType = $derived(
"type" in item && (item.type === "Audio" || item.type === "MusicAlbum" || item.type === "MusicArtist" || item.type === "Playlist")
);
const aspectRatio = $derived(() => {
if ("type" in item) {
// MediaItem
return item.type === "Audio" || item.type === "MusicAlbum" ? "aspect-square" : "aspect-[2/3]";
return isMusicType ? "aspect-square" : "aspect-[2/3]";
}
// Library
return "aspect-video";
return "collectionType" in item && item.collectionType === "music" ? "aspect-square" : "aspect-video";
});
const imageTag = $derived(
@@ -133,10 +133,11 @@
{#if loading}
<!-- Skeleton loading state -->
{@const isMusicContent = itemType === "MusicAlbum" || itemType === "Audio"}
<div class="grid grid-cols-2 md:grid-cols-6 gap-4">
{#each Array(6) as _}
<div class="animate-pulse">
<div class="aspect-[2/3] bg-[var(--color-surface)] rounded-lg mb-2"></div>
<div class="{isMusicContent ? 'aspect-square' : 'aspect-[2/3]'} bg-[var(--color-surface)] rounded-lg mb-2"></div>
<div class="h-4 bg-[var(--color-surface)] rounded w-3/4 mb-1"></div>
<div class="h-3 bg-[var(--color-surface)] rounded w-1/2"></div>
</div>
+32 -9
View File
@@ -40,7 +40,7 @@
let controlsTimeout: ReturnType<typeof setTimeout> | null = null;
let seekOffset = $state(0); // Track offset when seeking in transcoded streams
let isSeeking = $state(false);
let currentStreamUrl = $derived(streamUrl);
let currentStreamUrl = $state(streamUrl);
let hasReportedStart = $state(false);
let progressInterval: ReturnType<typeof setInterval> | null = null;
let isMediaReady = $state(false); // Track if media is ready to play (implements Loading state from DR-001)
@@ -67,6 +67,7 @@
let didStopBackendEarly = $state(false); // Track if we stopped backend early for non-transcoded content
let swipeType = $state<"brightness" | null>(null);
let hls: Hls | null = null; // HLS.js instance for streaming HLS content
let hlsFatalRecoveryAttempts = 0; // Track recovery attempts to prevent infinite restarts
// Audio track selection
let showAudioTrackMenu = $state(false);
@@ -260,14 +261,36 @@
console.log('[VideoPlayer] HLS manifest parsed, ready to play');
});
// Reset recovery attempts for new HLS instance
hlsFatalRecoveryAttempts = 0;
// Handle errors
hls.on(Hls.Events.ERROR, (event, data) => {
console.error('[VideoPlayer] HLS error:', data);
if (data.fatal) {
// Check if we're near the end of the video - if so, this is likely
// end-of-stream rather than a real error. Jellyfin transcoded HLS
// streams may not always terminate cleanly with #EXT-X-ENDLIST.
const knownDuration = media?.runTimeTicks ? media.runTimeTicks / 10_000_000 : videoDuration;
const effectiveTime = currentTime + seekOffset;
const isNearEnd = knownDuration > 0 && effectiveTime > 0 && effectiveTime / knownDuration > 0.9;
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
console.error('[VideoPlayer] Fatal network error, trying to recover');
hls!.startLoad();
hlsFatalRecoveryAttempts++;
if (isNearEnd) {
// Near end of stream - treat as natural end, don't restart
console.log('[VideoPlayer] Fatal network error near end of stream - treating as ended');
if (onEnded) {
onEnded();
}
} else if (hlsFatalRecoveryAttempts <= 3) {
console.error('[VideoPlayer] Fatal network error, trying to recover (attempt', hlsFatalRecoveryAttempts, ')');
hls!.startLoad();
} else {
console.error('[VideoPlayer] Fatal network error, max recovery attempts reached');
hls!.destroy();
}
break;
case Hls.ErrorTypes.MEDIA_ERROR:
console.error('[VideoPlayer] Fatal media error, trying to recover');
@@ -528,17 +551,17 @@
// Smooth time updates using requestAnimationFrame (60fps)
function updateTimeLoop() {
if (videoElement && !isSeeking && !isDraggingSeekBar && isPlaying) {
// Add seek offset to get actual position in the full video
const newCurrentTime = seekOffset + videoElement.currentTime;
// Safety check: only update if the video element is actually playing
// This prevents stale data from overwriting currentTime after failed seeks
if (videoElement.readyState >= 2) { // HAVE_CURRENT_DATA or better
if (videoElement.readyState >= 2) {
currentTime = newCurrentTime;
}
}
// Continue the loop while playing
// Keep the loop alive while playing — stopped by handlePause/handleEnded
if (isPlaying) {
rafId = requestAnimationFrame(updateTimeLoop);
} else {
rafId = null;
}
}