fix: Autoplay now resets time to zero and ignores trigger if episode already started (#3)
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 3m48s
Traceability Validation / Check Requirement Traces (push) Successful in 22s
Build & Release / Run Tests (push) Successful in 3m27s
🏗️ Build and Test JellyTau / Build Android APK (push) Successful in 18m31s
Build & Release / Build Linux (push) Successful in 15m52s
Build & Release / Build Android (push) Successful in 18m43s
Build & Release / Create Release (push) Successful in 12s

Reviewed-on: #3
Co-authored-by: Duncan Tourolle <duncan@tourolle.paris>
Co-committed-by: Duncan Tourolle <duncan@tourolle.paris>
This commit was merged in pull request #3.
This commit is contained in:
2026-06-23 21:12:01 +00:00
committed by dtourolle
parent 674c8e5cd0
commit dcf08f30bc
4 changed files with 74 additions and 33 deletions
+19 -9
View File
@@ -1,6 +1,6 @@
<!-- TRACES: UR-003, UR-005, UR-020, UR-021, UR-026 | DR-010, DR-023, DR-024 -->
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import { onMount, onDestroy, untrack } from "svelte";
import { commands } from "$lib/api/bindings";
import { listen } from "@tauri-apps/api/event";
import Hls from "hls.js";
@@ -20,9 +20,13 @@
needsTranscoding?: boolean; // Whether content needs transcoding (HEVC/10-bit) - affects seeking behavior
onClose: () => void;
onSeek?: (positionSeconds: number, audioStreamIndex?: number) => Promise<string>; // Returns new stream URL for transcoded seeking
onReportProgress?: (positionSeconds: number, isPaused: boolean) => void;
onReportStart?: (positionSeconds: number) => void;
onReportStop?: (positionSeconds: number) => void;
// Reporting callbacks pass the played media's id explicitly so a late
// reportStop (fired from onDestroy during autoplay navigation) is attributed
// to the episode this player actually played, not the next episode whose URL
// is already active on the page.
onReportProgress?: (positionSeconds: number, isPaused: boolean, reportId?: string) => void;
onReportStart?: (positionSeconds: number, reportId?: string) => void;
onReportStop?: (positionSeconds: number, reportId?: string) => void;
onEnded?: () => void; // Called when video playback ends naturally
onNext?: () => void; // Called when user clicks next episode button
hasNext?: boolean; // Whether there is a next episode available
@@ -30,6 +34,12 @@
let { media, streamUrl, mediaSourceId, initialPosition, needsTranscoding = false, onClose, onSeek, onReportProgress, onReportStart, onReportStop, onEnded, onNext, hasNext = false }: Props = $props();
// The id this player instance reports progress against. Snapshotted from the
// media prop so a late reportStop (e.g. from onDestroy during autoplay
// navigation) is always attributed to the episode this player played.
// untrack() makes the intent explicit: capture the initial value only.
const reportMediaId = untrack(() => media?.id);
let videoElement: HTMLVideoElement | null = $state(null);
let isPlaying = $state(false);
let currentTime = $state(0);
@@ -470,7 +480,7 @@
// Report progress every 10 seconds while playing
progressInterval = setInterval(() => {
if (isPlaying && !isSeeking && onReportProgress) {
onReportProgress(currentTime, false);
onReportProgress(currentTime, false, reportMediaId);
}
}, 10000);
@@ -536,7 +546,7 @@
// Report stop when component is destroyed
if (onReportStop && currentTime > 0) {
onReportStop(currentTime);
onReportStop(currentTime, reportMediaId);
}
});
@@ -730,7 +740,7 @@
startTimeUpdates(); // Start RAF loop for smooth time updates
// Report playback start on first play
if (!hasReportedStart && onReportStart) {
onReportStart(currentTime);
onReportStart(currentTime, reportMediaId);
hasReportedStart = true;
}
}
@@ -740,7 +750,7 @@
stopTimeUpdates(); // Stop RAF loop when paused
// Report progress when paused
if (onReportProgress) {
onReportProgress(currentTime, true);
onReportProgress(currentTime, true, reportMediaId);
}
}
@@ -749,7 +759,7 @@
stopTimeUpdates(); // Stop RAF loop when ended
// Report stop when video ends
if (onReportStop) {
onReportStop(currentTime);
onReportStop(currentTime, reportMediaId);
}
// Notify parent that video has ended (for next episode popup)
if (onEnded) {
+5 -1
View File
@@ -39,6 +39,10 @@ export async function cancelAutoPlay() {
/**
* Navigate to the next episode via goto().
* Uses replaceState to prevent history buildup when auto-advancing.
*
* Advancing to a next episode always starts that episode from the
* beginning, even if it was previously started or watched. The `restart`
* query param signals the player page to skip the resume-progress check.
*/
function navigateToEpisode(episode: MediaItem) {
if (isNavigating) {
@@ -48,7 +52,7 @@ function navigateToEpisode(episode: MediaItem) {
isNavigating = true;
console.log("[NextEpisode] Navigating to next episode:", episode.id, episode.name);
nextEpisode.hidePopup();
goto(`/player/${episode.id}`, { replaceState: true }).finally(() => {
goto(`/player/${episode.id}?restart=true`, { replaceState: true }).finally(() => {
isNavigating = false;
});
}