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
+35 -16
View File
@@ -25,6 +25,9 @@
const itemId = $derived($page.params.id);
const queueParam = $derived($page.url.searchParams.get("queue"));
const shuffleParam = $derived($page.url.searchParams.get("shuffle") === "true");
// When advancing to a next episode we always start from the beginning,
// even if the episode was previously started or watched.
const restartParam = $derived($page.url.searchParams.get("restart") === "true");
// Derive playback context from URL query params
const playbackContext = $derived.by(() => {
@@ -80,9 +83,12 @@
// Load when itemId changes (handles both initial load and navigation)
$effect(() => {
const id = itemId;
const restart = restartParam;
if (id && id !== loadedItemId) {
console.log("[AutoPlay] $effect triggered: loading new item", id, "(was:", loadedItemId, ")");
loadAndPlay(id);
console.log("[AutoPlay] $effect triggered: loading new item", id, "(was:", loadedItemId, ") restart:", restart);
// restart=true (advancing to next episode) forces start-from-beginning,
// bypassing the resume-progress check.
loadAndPlay(id, restart ? 0 : undefined, restart);
}
});
@@ -96,7 +102,7 @@
}
});
async function loadAndPlay(id: string, startPosition?: number) {
async function loadAndPlay(id: string, startPosition?: number, forceRestart = false) {
loading = true;
error = null;
loadedItemId = id;
@@ -118,9 +124,11 @@
}
// If this track is already playing in the backend, just show the UI
// without restarting playback (e.g., when expanding from MiniPlayer)
// without restarting playback (e.g., when expanding from MiniPlayer).
// forceRestart bypasses this so advancing to the next episode always
// restarts from the beginning even if it were already loaded.
const alreadyPlayingMedia = get(storeCurrentMedia);
if (alreadyPlayingMedia?.id === id && !startPosition) {
if (alreadyPlayingMedia?.id === id && !startPosition && !forceRestart) {
console.log("loadAndPlay: Track already playing, showing UI without restarting");
isVideo = item.type === "Movie" || item.type === "Episode";
isPlaying = true;
@@ -155,11 +163,13 @@
}
}
// Check for saved progress if no start position specified
// Check for saved progress if no start position specified.
// When forceRestart is set (advancing to a next episode) we always start
// from the beginning, skipping the resume check and resume dialog.
const userId = auth.getUserId();
console.log("Resume check - userId:", userId, "itemId:", id, "startPosition:", startPosition);
console.log("Resume check - userId:", userId, "itemId:", id, "startPosition:", startPosition, "forceRestart:", forceRestart);
if (!startPosition && userId) {
if (!startPosition && !forceRestart && userId) {
try {
const progress = await commands.storageGetPlaybackProgress(userId, id);
console.log("Resume check - retrieved progress:", progress);
@@ -471,23 +481,30 @@
}
// Playback reporting callbacks
function handleReportStart(positionSeconds: number) {
const id = itemId;
//
// These receive the reporting item id from the VideoPlayer (its own media.id),
// NOT the live URL param (itemId). During autoplay the URL flips to the next
// episode before the outgoing VideoPlayer's onDestroy fires its final
// reportStop. Keying off itemId would stamp the old episode's near-end
// position onto the new episode, making it resume at ~99% (or pop the resume
// dialog). The VideoPlayer always knows which media it actually played.
function handleReportStart(positionSeconds: number, reportId?: string) {
const id = reportId ?? itemId;
const context = playbackContext; // playbackContext is a derived value, not a function
if (id) {
reportPlaybackStart(id, positionSeconds, context.type, context.id);
}
}
function handleReportProgress(positionSeconds: number, isPaused: boolean) {
const id = itemId;
function handleReportProgress(positionSeconds: number, isPaused: boolean, reportId?: string) {
const id = reportId ?? itemId;
if (id) {
reportPlaybackProgress(id, positionSeconds, isPaused);
}
}
function handleReportStop(positionSeconds: number) {
const id = itemId;
function handleReportStop(positionSeconds: number, reportId?: string) {
const id = reportId ?? itemId;
if (id) {
reportPlaybackStopped(id, positionSeconds);
}
@@ -538,8 +555,10 @@
function handleSkipToNextEpisode() {
if (nextEpisode) {
// Use replaceState so "close/back" returns to the library, not the previous episode
goto(`/player/${nextEpisode.id}`, { replaceState: true });
// Use replaceState so "close/back" returns to the library, not the previous episode.
// restart=true so advancing to the next episode always starts from the beginning,
// even if it was previously started or watched.
goto(`/player/${nextEpisode.id}?restart=true`, { replaceState: true });
}
}