fix: episode transition, progress save, and seek alignment
This commit is contained in:
@@ -131,10 +131,11 @@ func (h *PlaybackHandler) HandleEpisodeData(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"mode_sources": modeSources,
|
||||
"available_modes": availableModes,
|
||||
"segments": segments,
|
||||
"episode_title": episodeTitle,
|
||||
"mode_sources": modeSources,
|
||||
"available_modes": availableModes,
|
||||
"start_time_seconds": watchData["StartTimeSeconds"],
|
||||
"segments": segments,
|
||||
"episode_title": episodeTitle,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -55,14 +55,19 @@ export const goToNextEpisode = async (): Promise<void> => {
|
||||
return;
|
||||
}
|
||||
|
||||
state.currentEpisode = String(nextEp);
|
||||
state.currentMode = fallback;
|
||||
// The progress reset is sent asynchronously, so do not trust the fetch to observe it first.
|
||||
state.startTimeSeconds = 0;
|
||||
state.container.dataset.currentEpisode = state.currentEpisode;
|
||||
state.container.dataset.startTimeSeconds = String(state.startTimeSeconds);
|
||||
|
||||
// load new video (keep preferences)
|
||||
const preferredQuality = localStorage.getItem('mal:preferred-quality') || 'best';
|
||||
state.video.src = `${state.streamURL}?mode=${encodeURIComponent(fallback)}&token=${encodeURIComponent(state.modeSources[fallback].token)}${preferredQuality !== 'best' ? `&quality=${encodeURIComponent(preferredQuality)}` : ''}`;
|
||||
state.video.load();
|
||||
if (!state.video.paused) state.video.play().catch(() => {});
|
||||
|
||||
state.currentEpisode = String(nextEp);
|
||||
state.currentMode = fallback;
|
||||
state.pendingSeekTime = null;
|
||||
state.completionSent = false;
|
||||
state.completionAttempts = 0;
|
||||
@@ -103,7 +108,6 @@ export const goToNextEpisode = async (): Promise<void> => {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('ep', String(nextEp));
|
||||
history.pushState(null, '', url.toString());
|
||||
state.transitionEpisode = null;
|
||||
} catch {
|
||||
sessionStorage.setItem('mal:autoplay-next', 'true');
|
||||
const url = new URL(window.location.href);
|
||||
|
||||
@@ -11,7 +11,12 @@ import { goToNextEpisode } from './episodes/nav';
|
||||
import { resolveActiveSegments, renderSegments } from './skip/segments';
|
||||
import { setupThumbnails } from './episodes/thumbnails';
|
||||
import { markEpisodeTransition, setupProgress } from './progress';
|
||||
import { absoluteTimeFromRatio, getBounds, displayTimeFromAbsolute } from './timeline';
|
||||
import {
|
||||
absoluteTimeFromDisplay,
|
||||
absoluteTimeFromRatio,
|
||||
getBounds,
|
||||
displayTimeFromAbsolute,
|
||||
} from './timeline';
|
||||
import { formatTime } from './controls';
|
||||
|
||||
let initialized = false; // prevent double init on htmx swaps
|
||||
@@ -98,15 +103,18 @@ const initPlayer = (): void => {
|
||||
renderSegments();
|
||||
|
||||
// resume from saved position
|
||||
const startTime = Number(container.dataset.startTimeSeconds ?? '0');
|
||||
if (startTime > 0 && state.video.currentTime <= 0.5 && state.video.duration > startTime) {
|
||||
state.video.currentTime = startTime;
|
||||
const startTime = state.startTimeSeconds;
|
||||
if (startTime > 0 && state.video.currentTime <= 0.5 && getBounds().duration > startTime) {
|
||||
state.video.currentTime = absoluteTimeFromDisplay(startTime);
|
||||
}
|
||||
// resume after mode switch
|
||||
if (state.pendingSeekTime !== null) {
|
||||
state.video.currentTime = state.pendingSeekTime;
|
||||
state.video.currentTime = absoluteTimeFromDisplay(state.pendingSeekTime);
|
||||
state.pendingSeekTime = null;
|
||||
}
|
||||
if (state.transitionEpisode === Number.parseInt(state.currentEpisode, 10)) {
|
||||
state.transitionEpisode = null;
|
||||
}
|
||||
// autoplay if not already playing (inline script may have already called play())
|
||||
if (state.shouldAutoPlay || state.video.paused) state.video.play().catch(() => {});
|
||||
|
||||
@@ -184,14 +192,20 @@ const initPlayer = (): void => {
|
||||
updateSkipButton(state.video.currentTime);
|
||||
});
|
||||
|
||||
// track episode transitions from external links
|
||||
container.addEventListener('click', e => {
|
||||
const anchor = (e.target as Node).parentElement?.closest('a[href]');
|
||||
// track next-episode links outside the player so they start fresh after finishing an episode
|
||||
document.addEventListener('click', e => {
|
||||
const target = e.target;
|
||||
if (!(target instanceof Element)) return;
|
||||
const anchor = target.closest('a[href]');
|
||||
if (!(anchor instanceof HTMLAnchorElement)) return;
|
||||
const parts = new URL(anchor.href, location.origin).pathname.split('/').filter(Boolean);
|
||||
if (parts[0] === 'watch' && Number.parseInt(parts[2], 10) > 0) {
|
||||
markEpisodeTransition(Number.parseInt(parts[2], 10));
|
||||
}
|
||||
const url = new URL(anchor.href, location.origin);
|
||||
if (url.origin !== location.origin) return;
|
||||
const parts = url.pathname.split('/').filter(Boolean);
|
||||
if (parts[0] !== 'anime' || parts[2] !== 'watch') return;
|
||||
if (Number.parseInt(parts[1], 10) !== state.malID) return;
|
||||
const nextEpisode = Number.parseInt(url.searchParams.get('ep') ?? '1', 10);
|
||||
const currentEpisode = Number.parseInt(state.currentEpisode, 10);
|
||||
if (nextEpisode === currentEpisode + 1) markEpisodeTransition(nextEpisode);
|
||||
});
|
||||
|
||||
state.video.addEventListener('click', showControls);
|
||||
|
||||
@@ -21,7 +21,7 @@ const sendBeacon = (payload: string) => {
|
||||
* Debounced: skips if within 5s of last save for same episode.
|
||||
*/
|
||||
export const saveProgress = async (): Promise<void> => {
|
||||
if (!state.malID || state.video.currentTime < 1) return;
|
||||
if (state.transitionEpisode !== null || !state.malID || state.video.currentTime < 1) return;
|
||||
const episode = Number.parseInt(state.currentEpisode, 10);
|
||||
if (!episode) return;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface PlayerState {
|
||||
malID: number;
|
||||
streamURL: string;
|
||||
initialStreamToken: string;
|
||||
startTimeSeconds: number;
|
||||
shouldAutoPlay: boolean;
|
||||
parsedSegments: SkipSegment[];
|
||||
activeSegments: ActiveSegment[];
|
||||
@@ -56,6 +57,7 @@ export const state: PlayerState = {
|
||||
malID: 0,
|
||||
streamURL: '/watch/proxy/stream',
|
||||
initialStreamToken: '',
|
||||
startTimeSeconds: 0,
|
||||
shouldAutoPlay: false,
|
||||
parsedSegments: [],
|
||||
activeSegments: [],
|
||||
@@ -102,6 +104,7 @@ export const initState = (c: HTMLElement): void => {
|
||||
state.totalEpisodes = Number.parseInt(dataset(c, 'totalEpisodes'), 10);
|
||||
state.streamURL = dataset(c, 'streamUrl') || '/watch/proxy/stream';
|
||||
state.initialStreamToken = dataset(c, 'streamToken') || '';
|
||||
state.startTimeSeconds = Number.parseFloat(dataset(c, 'startTimeSeconds') || '0') || 0;
|
||||
// from session: previous page set this when autoplay triggered
|
||||
state.shouldAutoPlay = sessionStorage.getItem('mal:autoplay-next') === 'true';
|
||||
sessionStorage.removeItem('mal:autoplay-next');
|
||||
|
||||
Reference in New Issue
Block a user