diff --git a/changes/fix-anilist-timepos-threshold.md b/changes/fix-anilist-timepos-threshold.md new file mode 100644 index 00000000..4bc1c0a3 --- /dev/null +++ b/changes/fix-anilist-timepos-threshold.md @@ -0,0 +1,4 @@ +type: fixed +area: anilist + +- Used fresh mpv time-position events for AniList post-watch threshold checks so progress updates still fire when playback reaches the watched threshold. diff --git a/src/main.ts b/src/main.ts index 01413126..8a9604c0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3986,7 +3986,7 @@ const { reportJellyfinRemoteStopped: () => { void reportJellyfinRemoteStopped(); }, - maybeRunAnilistPostWatchUpdate: () => maybeRunAnilistPostWatchUpdate(), + maybeRunAnilistPostWatchUpdate: (options) => maybeRunAnilistPostWatchUpdate(options), logSubtitleTimingError: (message, error) => logger.error(message, error), broadcastToOverlayWindows: (channel, payload) => { broadcastToOverlayWindows(channel, payload); diff --git a/src/main/runtime/anilist-post-watch.test.ts b/src/main/runtime/anilist-post-watch.test.ts index 83923a11..7fa3c529 100644 --- a/src/main/runtime/anilist-post-watch.test.ts +++ b/src/main/runtime/anilist-post-watch.test.ts @@ -121,6 +121,46 @@ test('createMaybeRunAnilistPostWatchUpdateHandler force-runs manual watched upda assert.ok(calls.includes('osd:updated ok')); }); +test('createMaybeRunAnilistPostWatchUpdateHandler uses provided watched seconds from time-position events', async () => { + const calls: string[] = []; + const handler = createMaybeRunAnilistPostWatchUpdateHandler({ + getInFlight: () => false, + setInFlight: (value) => calls.push(`inflight:${value}`), + getResolvedConfig: () => ({}), + isAnilistTrackingEnabled: () => true, + getCurrentMediaKey: () => '/tmp/video.mkv', + hasMpvClient: () => true, + getTrackedMediaKey: () => '/tmp/video.mkv', + resetTrackedMedia: () => {}, + getWatchedSeconds: () => 0, + maybeProbeAnilistDuration: async () => 1000, + ensureAnilistMediaGuess: async () => ({ title: 'Show', season: null, episode: 8 }), + hasAttemptedUpdateKey: () => false, + processNextAnilistRetryUpdate: async () => ({ ok: true, message: 'noop' }), + refreshAnilistClientSecretState: async () => 'token', + enqueueRetry: () => calls.push('enqueue'), + markRetryFailure: () => calls.push('mark-failure'), + markRetrySuccess: () => calls.push('mark-success'), + refreshRetryQueueState: () => calls.push('refresh'), + updateAnilistPostWatchProgress: async () => { + calls.push('update'); + return { status: 'updated', message: 'updated ok' }; + }, + rememberAttemptedUpdateKey: () => calls.push('remember'), + showMpvOsd: (message) => calls.push(`osd:${message}`), + logInfo: (message) => calls.push(`info:${message}`), + logWarn: (message) => calls.push(`warn:${message}`), + minWatchSeconds: 600, + minWatchRatio: 0.85, + }); + + await handler({ watchedSeconds: 850 }); + + assert.ok(calls.includes('update')); + assert.ok(calls.includes('remember')); + assert.ok(calls.includes('osd:updated ok')); +}); + test('createMaybeRunAnilistPostWatchUpdateHandler blocks concurrent runs before async gating', async () => { const calls: string[] = []; let inFlight = false; diff --git a/src/main/runtime/anilist-post-watch.ts b/src/main/runtime/anilist-post-watch.ts index 47a9a3d4..dfad685c 100644 --- a/src/main/runtime/anilist-post-watch.ts +++ b/src/main/runtime/anilist-post-watch.ts @@ -18,6 +18,7 @@ type RetryQueueItem = { type AnilistPostWatchRunOptions = { force?: boolean; + watchedSeconds?: number; }; export function buildAnilistAttemptKey(mediaKey: string, episode: number): string { @@ -146,7 +147,10 @@ export function createMaybeRunAnilistPostWatchUpdateHandler(deps: { let watchedSeconds = 0; if (!force) { - watchedSeconds = deps.getWatchedSeconds(); + watchedSeconds = + typeof options.watchedSeconds === 'number' && Number.isFinite(options.watchedSeconds) + ? options.watchedSeconds + : deps.getWatchedSeconds(); if (!Number.isFinite(watchedSeconds) || watchedSeconds < deps.minWatchSeconds) { return; } diff --git a/src/main/runtime/mpv-main-event-actions.test.ts b/src/main/runtime/mpv-main-event-actions.test.ts index dc862687..604301c0 100644 --- a/src/main/runtime/mpv-main-event-actions.test.ts +++ b/src/main/runtime/mpv-main-event-actions.test.ts @@ -223,6 +223,23 @@ test('time-pos and pause handlers report progress with correct urgency', () => { ]); }); +test('time-pos handler passes fresh playback time to AniList post-watch', async () => { + const watchedSeconds: unknown[] = []; + const timeHandler = createHandleMpvTimePosChangeHandler({ + recordPlaybackPosition: () => {}, + reportJellyfinRemoteProgress: () => {}, + refreshDiscordPresence: () => {}, + maybeRunAnilistPostWatchUpdate: async (options) => { + watchedSeconds.push(options?.watchedSeconds); + }, + }); + + timeHandler({ time: 850 }); + await Promise.resolve(); + + assert.deepEqual(watchedSeconds, [850]); +}); + test('time-pos handler logs post-watch update rejection without blocking later handlers', async () => { const calls: string[] = []; const timeHandler = createHandleMpvTimePosChangeHandler({ diff --git a/src/main/runtime/mpv-main-event-actions.ts b/src/main/runtime/mpv-main-event-actions.ts index b2a5952a..d255cf12 100644 --- a/src/main/runtime/mpv-main-event-actions.ts +++ b/src/main/runtime/mpv-main-event-actions.ts @@ -1,5 +1,9 @@ import type { SubtitleData } from '../../types'; +type AnilistPostWatchRunOptions = { + watchedSeconds?: number; +}; + export function createHandleMpvSubtitleChangeHandler(deps: { setCurrentSubText: (text: string) => void; getImmediateSubtitlePayload?: (text: string) => SubtitleData | null; @@ -105,7 +109,7 @@ export function createHandleMpvTimePosChangeHandler(deps: { recordPlaybackPosition: (time: number) => void; reportJellyfinRemoteProgress: (forceImmediate: boolean) => void; refreshDiscordPresence: () => void; - maybeRunAnilistPostWatchUpdate?: () => Promise; + maybeRunAnilistPostWatchUpdate?: (options?: AnilistPostWatchRunOptions) => Promise; logError?: (message: string, error: unknown) => void; onTimePosUpdate?: (time: number) => void; }) { @@ -113,7 +117,7 @@ export function createHandleMpvTimePosChangeHandler(deps: { deps.recordPlaybackPosition(time); deps.reportJellyfinRemoteProgress(false); deps.refreshDiscordPresence(); - void deps.maybeRunAnilistPostWatchUpdate?.().catch((error) => { + void deps.maybeRunAnilistPostWatchUpdate?.({ watchedSeconds: time }).catch((error) => { deps.logError?.('AniList post-watch update failed unexpectedly', error); }); deps.onTimePosUpdate?.(time); diff --git a/src/main/runtime/mpv-main-event-bindings.ts b/src/main/runtime/mpv-main-event-bindings.ts index 4641b983..5ac92d69 100644 --- a/src/main/runtime/mpv-main-event-bindings.ts +++ b/src/main/runtime/mpv-main-event-bindings.ts @@ -18,6 +18,10 @@ import { type MpvEventClient = Parameters>[0]; +type AnilistPostWatchRunOptions = { + watchedSeconds?: number; +}; + export function createBindMpvMainEventHandlersHandler(deps: { reportJellyfinRemoteStopped: () => void; syncOverlayMpvSubtitleSuppression: () => void; @@ -34,7 +38,7 @@ export function createBindMpvMainEventHandlersHandler(deps: { recordImmersionSubtitleLine: (text: string, start: number, end: number) => void; hasSubtitleTimingTracker: () => boolean; recordSubtitleTiming: (text: string, start: number, end: number) => void; - maybeRunAnilistPostWatchUpdate: () => Promise; + maybeRunAnilistPostWatchUpdate: (options?: AnilistPostWatchRunOptions) => Promise; logSubtitleTimingError: (message: string, error: unknown) => void; setCurrentSubText: (text: string) => void; @@ -149,7 +153,7 @@ export function createBindMpvMainEventHandlersHandler(deps: { reportJellyfinRemoteProgress: (forceImmediate) => deps.reportJellyfinRemoteProgress(forceImmediate), refreshDiscordPresence: () => deps.refreshDiscordPresence(), - maybeRunAnilistPostWatchUpdate: () => deps.maybeRunAnilistPostWatchUpdate(), + maybeRunAnilistPostWatchUpdate: (options) => deps.maybeRunAnilistPostWatchUpdate(options), logError: (message, error) => deps.logSubtitleTimingError(message, error), onTimePosUpdate: (time) => deps.onTimePosUpdate?.(time), }); diff --git a/src/main/runtime/mpv-main-event-main-deps.ts b/src/main/runtime/mpv-main-event-main-deps.ts index a703ba13..6a1c4bb0 100644 --- a/src/main/runtime/mpv-main-event-main-deps.ts +++ b/src/main/runtime/mpv-main-event-main-deps.ts @@ -1,5 +1,9 @@ import type { MergedToken, SubtitleData } from '../../types'; +type AnilistPostWatchRunOptions = { + watchedSeconds?: number; +}; + export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: { appState: { initialArgs?: { @@ -42,7 +46,7 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: { quitApp: () => void; reportJellyfinRemoteStopped: () => void; syncOverlayMpvSubtitleSuppression: () => void; - maybeRunAnilistPostWatchUpdate: () => Promise; + maybeRunAnilistPostWatchUpdate: (options?: AnilistPostWatchRunOptions) => Promise; logSubtitleTimingError: (message: string, error: unknown) => void; broadcastToOverlayWindows: (channel: string, payload: unknown) => void; getImmediateSubtitlePayload?: (text: string) => SubtitleData | null; @@ -126,7 +130,8 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: { hasSubtitleTimingTracker: () => Boolean(deps.appState.subtitleTimingTracker), recordSubtitleTiming: (text: string, start: number, end: number) => deps.appState.subtitleTimingTracker?.recordSubtitle?.(text, start, end), - maybeRunAnilistPostWatchUpdate: () => deps.maybeRunAnilistPostWatchUpdate(), + maybeRunAnilistPostWatchUpdate: (options?: AnilistPostWatchRunOptions) => + deps.maybeRunAnilistPostWatchUpdate(options), logSubtitleTimingError: (message: string, error: unknown) => deps.logSubtitleTimingError(message, error), setCurrentSubText: (text: string) => {