fix: Autoplay now resets time to zero and ignores trigger if episode already started (#3)
All checks were successful
🏗️ 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
All checks were successful
🏗️ 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 is contained in:
parent
674c8e5cd0
commit
dcf08f30bc
@ -95,20 +95,28 @@ jobs:
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Check each file
|
# Check each file
|
||||||
MISSING_TRACES=0
|
# Pipe into the loop instead of a here-string (<<<) so this step works
|
||||||
while IFS= read -r file; do
|
# under POSIX sh/dash, not just bash. Use `case` instead of `[[ == ]]`
|
||||||
|
# for the same reason. The loop runs in a subshell (so a counter var
|
||||||
|
# wouldn't survive), so we record warnings in a temp file and count it
|
||||||
|
# afterwards.
|
||||||
|
MISSING_FILE=$(mktemp)
|
||||||
|
echo "$CHANGED" | while IFS= read -r file; do
|
||||||
# Skip test files
|
# Skip test files
|
||||||
if [[ "$file" == *".test."* ]]; then
|
case "$file" in
|
||||||
continue
|
*.test.*) continue ;;
|
||||||
fi
|
esac
|
||||||
|
|
||||||
if [ -f "$file" ]; then
|
if [ -f "$file" ]; then
|
||||||
if ! grep -q "TRACES:" "$file"; then
|
if ! grep -q "TRACES:" "$file"; then
|
||||||
echo "⚠️ Missing TRACES: $file"
|
echo "⚠️ Missing TRACES: $file"
|
||||||
MISSING_TRACES=$((MISSING_TRACES + 1))
|
echo "$file" >> "$MISSING_FILE"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done <<< "$CHANGED"
|
done
|
||||||
|
|
||||||
|
MISSING_TRACES=$(wc -l < "$MISSING_FILE" | tr -d ' ')
|
||||||
|
rm -f "$MISSING_FILE"
|
||||||
|
|
||||||
if [ "$MISSING_TRACES" -gt 0 ]; then
|
if [ "$MISSING_TRACES" -gt 0 ]; then
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<!-- TRACES: UR-003, UR-005, UR-020, UR-021, UR-026 | DR-010, DR-023, DR-024 -->
|
<!-- TRACES: UR-003, UR-005, UR-020, UR-021, UR-026 | DR-010, DR-023, DR-024 -->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, onDestroy } from "svelte";
|
import { onMount, onDestroy, untrack } from "svelte";
|
||||||
import { commands } from "$lib/api/bindings";
|
import { commands } from "$lib/api/bindings";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import Hls from "hls.js";
|
import Hls from "hls.js";
|
||||||
@ -20,9 +20,13 @@
|
|||||||
needsTranscoding?: boolean; // Whether content needs transcoding (HEVC/10-bit) - affects seeking behavior
|
needsTranscoding?: boolean; // Whether content needs transcoding (HEVC/10-bit) - affects seeking behavior
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSeek?: (positionSeconds: number, audioStreamIndex?: number) => Promise<string>; // Returns new stream URL for transcoded seeking
|
onSeek?: (positionSeconds: number, audioStreamIndex?: number) => Promise<string>; // Returns new stream URL for transcoded seeking
|
||||||
onReportProgress?: (positionSeconds: number, isPaused: boolean) => void;
|
// Reporting callbacks pass the played media's id explicitly so a late
|
||||||
onReportStart?: (positionSeconds: number) => void;
|
// reportStop (fired from onDestroy during autoplay navigation) is attributed
|
||||||
onReportStop?: (positionSeconds: number) => void;
|
// 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
|
onEnded?: () => void; // Called when video playback ends naturally
|
||||||
onNext?: () => void; // Called when user clicks next episode button
|
onNext?: () => void; // Called when user clicks next episode button
|
||||||
hasNext?: boolean; // Whether there is a next episode available
|
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();
|
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 videoElement: HTMLVideoElement | null = $state(null);
|
||||||
let isPlaying = $state(false);
|
let isPlaying = $state(false);
|
||||||
let currentTime = $state(0);
|
let currentTime = $state(0);
|
||||||
@ -470,7 +480,7 @@
|
|||||||
// Report progress every 10 seconds while playing
|
// Report progress every 10 seconds while playing
|
||||||
progressInterval = setInterval(() => {
|
progressInterval = setInterval(() => {
|
||||||
if (isPlaying && !isSeeking && onReportProgress) {
|
if (isPlaying && !isSeeking && onReportProgress) {
|
||||||
onReportProgress(currentTime, false);
|
onReportProgress(currentTime, false, reportMediaId);
|
||||||
}
|
}
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
@ -536,7 +546,7 @@
|
|||||||
|
|
||||||
// Report stop when component is destroyed
|
// Report stop when component is destroyed
|
||||||
if (onReportStop && currentTime > 0) {
|
if (onReportStop && currentTime > 0) {
|
||||||
onReportStop(currentTime);
|
onReportStop(currentTime, reportMediaId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -730,7 +740,7 @@
|
|||||||
startTimeUpdates(); // Start RAF loop for smooth time updates
|
startTimeUpdates(); // Start RAF loop for smooth time updates
|
||||||
// Report playback start on first play
|
// Report playback start on first play
|
||||||
if (!hasReportedStart && onReportStart) {
|
if (!hasReportedStart && onReportStart) {
|
||||||
onReportStart(currentTime);
|
onReportStart(currentTime, reportMediaId);
|
||||||
hasReportedStart = true;
|
hasReportedStart = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -740,7 +750,7 @@
|
|||||||
stopTimeUpdates(); // Stop RAF loop when paused
|
stopTimeUpdates(); // Stop RAF loop when paused
|
||||||
// Report progress when paused
|
// Report progress when paused
|
||||||
if (onReportProgress) {
|
if (onReportProgress) {
|
||||||
onReportProgress(currentTime, true);
|
onReportProgress(currentTime, true, reportMediaId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -749,7 +759,7 @@
|
|||||||
stopTimeUpdates(); // Stop RAF loop when ended
|
stopTimeUpdates(); // Stop RAF loop when ended
|
||||||
// Report stop when video ends
|
// Report stop when video ends
|
||||||
if (onReportStop) {
|
if (onReportStop) {
|
||||||
onReportStop(currentTime);
|
onReportStop(currentTime, reportMediaId);
|
||||||
}
|
}
|
||||||
// Notify parent that video has ended (for next episode popup)
|
// Notify parent that video has ended (for next episode popup)
|
||||||
if (onEnded) {
|
if (onEnded) {
|
||||||
|
|||||||
@ -39,6 +39,10 @@ export async function cancelAutoPlay() {
|
|||||||
/**
|
/**
|
||||||
* Navigate to the next episode via goto().
|
* Navigate to the next episode via goto().
|
||||||
* Uses replaceState to prevent history buildup when auto-advancing.
|
* 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) {
|
function navigateToEpisode(episode: MediaItem) {
|
||||||
if (isNavigating) {
|
if (isNavigating) {
|
||||||
@ -48,7 +52,7 @@ function navigateToEpisode(episode: MediaItem) {
|
|||||||
isNavigating = true;
|
isNavigating = true;
|
||||||
console.log("[NextEpisode] Navigating to next episode:", episode.id, episode.name);
|
console.log("[NextEpisode] Navigating to next episode:", episode.id, episode.name);
|
||||||
nextEpisode.hidePopup();
|
nextEpisode.hidePopup();
|
||||||
goto(`/player/${episode.id}`, { replaceState: true }).finally(() => {
|
goto(`/player/${episode.id}?restart=true`, { replaceState: true }).finally(() => {
|
||||||
isNavigating = false;
|
isNavigating = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,9 @@
|
|||||||
const itemId = $derived($page.params.id);
|
const itemId = $derived($page.params.id);
|
||||||
const queueParam = $derived($page.url.searchParams.get("queue"));
|
const queueParam = $derived($page.url.searchParams.get("queue"));
|
||||||
const shuffleParam = $derived($page.url.searchParams.get("shuffle") === "true");
|
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
|
// Derive playback context from URL query params
|
||||||
const playbackContext = $derived.by(() => {
|
const playbackContext = $derived.by(() => {
|
||||||
@ -80,9 +83,12 @@
|
|||||||
// Load when itemId changes (handles both initial load and navigation)
|
// Load when itemId changes (handles both initial load and navigation)
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const id = itemId;
|
const id = itemId;
|
||||||
|
const restart = restartParam;
|
||||||
if (id && id !== loadedItemId) {
|
if (id && id !== loadedItemId) {
|
||||||
console.log("[AutoPlay] $effect triggered: loading new item", id, "(was:", loadedItemId, ")");
|
console.log("[AutoPlay] $effect triggered: loading new item", id, "(was:", loadedItemId, ") restart:", restart);
|
||||||
loadAndPlay(id);
|
// 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;
|
loading = true;
|
||||||
error = null;
|
error = null;
|
||||||
loadedItemId = id;
|
loadedItemId = id;
|
||||||
@ -118,9 +124,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If this track is already playing in the backend, just show the UI
|
// 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);
|
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");
|
console.log("loadAndPlay: Track already playing, showing UI without restarting");
|
||||||
isVideo = item.type === "Movie" || item.type === "Episode";
|
isVideo = item.type === "Movie" || item.type === "Episode";
|
||||||
isPlaying = true;
|
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();
|
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 {
|
try {
|
||||||
const progress = await commands.storageGetPlaybackProgress(userId, id);
|
const progress = await commands.storageGetPlaybackProgress(userId, id);
|
||||||
console.log("Resume check - retrieved progress:", progress);
|
console.log("Resume check - retrieved progress:", progress);
|
||||||
@ -471,23 +481,30 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Playback reporting callbacks
|
// 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
|
const context = playbackContext; // playbackContext is a derived value, not a function
|
||||||
if (id) {
|
if (id) {
|
||||||
reportPlaybackStart(id, positionSeconds, context.type, context.id);
|
reportPlaybackStart(id, positionSeconds, context.type, context.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleReportProgress(positionSeconds: number, isPaused: boolean) {
|
function handleReportProgress(positionSeconds: number, isPaused: boolean, reportId?: string) {
|
||||||
const id = itemId;
|
const id = reportId ?? itemId;
|
||||||
if (id) {
|
if (id) {
|
||||||
reportPlaybackProgress(id, positionSeconds, isPaused);
|
reportPlaybackProgress(id, positionSeconds, isPaused);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleReportStop(positionSeconds: number) {
|
function handleReportStop(positionSeconds: number, reportId?: string) {
|
||||||
const id = itemId;
|
const id = reportId ?? itemId;
|
||||||
if (id) {
|
if (id) {
|
||||||
reportPlaybackStopped(id, positionSeconds);
|
reportPlaybackStopped(id, positionSeconds);
|
||||||
}
|
}
|
||||||
@ -538,8 +555,10 @@
|
|||||||
|
|
||||||
function handleSkipToNextEpisode() {
|
function handleSkipToNextEpisode() {
|
||||||
if (nextEpisode) {
|
if (nextEpisode) {
|
||||||
// Use replaceState so "close/back" returns to the library, not the previous episode
|
// Use replaceState so "close/back" returns to the library, not the previous episode.
|
||||||
goto(`/player/${nextEpisode.id}`, { replaceState: true });
|
// 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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user