diff --git a/static/player/controls.ts b/static/player/controls.ts index f9e6df6..e3dbbbb 100644 --- a/static/player/controls.ts +++ b/static/player/controls.ts @@ -1,4 +1,5 @@ import { state } from './state'; +import { saveProgress } from './progress'; export const formatTime = (seconds: number): string => { if (!Number.isFinite(seconds) || seconds < 0) return '00:00'; @@ -203,6 +204,7 @@ export const setupControls = (): void => { state.video.addEventListener('pause', () => { updatePlayPauseIcons(false); showControls(); + void saveProgress(); }); state.video.addEventListener('volumechange', syncVolumeUI); diff --git a/static/player/keyboard.ts b/static/player/keyboard.ts index 3f571d8..ac1f5c1 100644 --- a/static/player/keyboard.ts +++ b/static/player/keyboard.ts @@ -8,6 +8,7 @@ import { seekBy, setVolume, } from './controls'; +import { saveProgress } from './progress'; /** * Sets up keyboard shortcuts for player control. @@ -26,6 +27,7 @@ export const setupKeyboard = (): void => { e.preventDefault(); togglePlayPause(); showControls(); + void saveProgress(); break; case 'ArrowLeft': case 'KeyJ': diff --git a/static/player/progress.ts b/static/player/progress.ts index 7e478db..d582c5c 100644 --- a/static/player/progress.ts +++ b/static/player/progress.ts @@ -16,35 +16,51 @@ const sendBeacon = (payload: string) => { return true; }; +let saveProgressInFlight: Promise | null = null; + /** * Saves current progress to backend. * Debounced: skips if within 5s of last save for same episode. */ export const saveProgress = async (): Promise => { - if (state.transitionEpisode !== null || !state.malID || state.video.currentTime < 1) return; - // progress is user-scoped; avoid spamming 401s for anonymous sessions - if (!document.cookie.includes('mal_session=')) return; - const episode = Number.parseInt(state.currentEpisode, 10); - if (!episode) return; + if (saveProgressInFlight) return saveProgressInFlight; - const safeTime = displayTimeFromAbsolute(state.video.currentTime); - // skip if recently saved - if ( - state.lastSavedProgress.episode === state.currentEpisode && - Math.abs(state.lastSavedProgress.seconds - safeTime) < 5 - ) - return; + const request = (async (): Promise => { + if (state.transitionEpisode !== null || !state.malID || state.video.currentTime < 1) return; + // progress is user-scoped; avoid spamming 401s for anonymous sessions + if (!document.cookie.includes('mal_session=')) return; + const episode = Number.parseInt(state.currentEpisode, 10); + if (!episode) return; - const payload = buildPayload(episode, safeTime); + const safeTime = displayTimeFromAbsolute(state.video.currentTime); + // skip if recently saved + if ( + state.lastSavedProgress.episode === state.currentEpisode && + Math.abs(state.lastSavedProgress.seconds - safeTime) < 5 + ) { + return; + } + + const payload = buildPayload(episode, safeTime); + try { + const res = await fetch('/api/watch-progress', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: payload, + }); + if (!res.ok) return; + state.lastSavedProgress = { episode: state.currentEpisode, seconds: safeTime }; + } catch {} + })(); + + saveProgressInFlight = request; try { - const res = await fetch('/api/watch-progress', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: payload, - }); - if (!res.ok) return; - state.lastSavedProgress = { episode: state.currentEpisode, seconds: safeTime }; - } catch {} + await request; + } finally { + if (saveProgressInFlight === request) { + saveProgressInFlight = null; + } + } }; // schedules periodic save every 30s during playback diff --git a/static/player/skip/index.ts b/static/player/skip/index.ts index b83bcab..cc08269 100644 --- a/static/player/skip/index.ts +++ b/static/player/skip/index.ts @@ -1,6 +1,7 @@ import { state } from '../state'; import { displayTimeFromAbsolute, absoluteTimeFromDisplay } from '../timeline'; import { showControls } from '../controls'; +import { saveProgress } from '../progress'; // button label based on segment type const skipLabel = (type: string): string => (type === 'ed' ? 'Skip outro' : 'Skip intro'); @@ -29,6 +30,7 @@ export const updateSkipButton = (currentTime: number): void => { const autoSkip = localStorage.getItem('mal:autoskip-enabled') === 'true'; if (autoSkip && displayTime >= segment.start && displayTime < segment.end) { state.video.currentTime = absoluteTimeFromDisplay(segment.end + 0.01); + void saveProgress(); return; }