fix: sync AniList after seeked completion

This commit is contained in:
2026-05-03 21:07:13 -07:00
parent 040741cf57
commit 69d5cc7557
10 changed files with 155 additions and 20 deletions
+2
View File
@@ -94,6 +94,7 @@ export interface MainIpcRuntimeServiceDepsParams {
openAnilistSetup: IpcDepsRuntimeOptions['openAnilistSetup'];
getAnilistQueueStatus: IpcDepsRuntimeOptions['getAnilistQueueStatus'];
retryAnilistQueueNow: IpcDepsRuntimeOptions['retryAnilistQueueNow'];
runAnilistPostWatchUpdateOnManualMark?: IpcDepsRuntimeOptions['runAnilistPostWatchUpdateOnManualMark'];
getCharacterDictionarySelection?: IpcDepsRuntimeOptions['getCharacterDictionarySelection'];
setCharacterDictionarySelection?: IpcDepsRuntimeOptions['setCharacterDictionarySelection'];
appendClipboardVideoToQueue: IpcDepsRuntimeOptions['appendClipboardVideoToQueue'];
@@ -263,6 +264,7 @@ export function createMainIpcRuntimeServiceDeps(
openAnilistSetup: params.openAnilistSetup,
getAnilistQueueStatus: params.getAnilistQueueStatus,
retryAnilistQueueNow: params.retryAnilistQueueNow,
runAnilistPostWatchUpdateOnManualMark: params.runAnilistPostWatchUpdateOnManualMark,
getCharacterDictionarySelection: params.getCharacterDictionarySelection,
setCharacterDictionarySelection: params.setCharacterDictionarySelection,
appendClipboardVideoToQueue: params.appendClipboardVideoToQueue,
@@ -77,6 +77,50 @@ test('createMaybeRunAnilistPostWatchUpdateHandler queues when token missing', as
assert.ok(calls.includes('inflight:false'));
});
test('createMaybeRunAnilistPostWatchUpdateHandler force-runs manual watched updates below threshold', async () => {
const calls: string[] = [];
const handler = createMaybeRunAnilistPostWatchUpdateHandler({
getInFlight: () => false,
setInFlight: (value) => calls.push(`inflight:${value}`),
getResolvedConfig: () => ({}),
isAnilistTrackingEnabled: () => true,
getCurrentMediaKey: () => '/tmp/video.mkv',
hasMpvClient: () => false,
getTrackedMediaKey: () => '/tmp/video.mkv',
resetTrackedMedia: () => {},
getWatchedSeconds: () => 0,
maybeProbeAnilistDuration: async () => {
calls.push('probe');
return 1000;
},
ensureAnilistMediaGuess: async () => ({ title: 'Show', episode: 3 }),
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({ force: true });
assert.equal(calls.includes('probe'), false);
assert.ok(calls.includes('update'));
assert.ok(calls.includes('remember'));
assert.ok(calls.includes('osd:updated ok'));
});
test('createMaybeRunAnilistPostWatchUpdateHandler skips youtube playback entirely', async () => {
const calls: string[] = [];
const handler = createMaybeRunAnilistPostWatchUpdateHandler({
+19 -12
View File
@@ -16,6 +16,10 @@ type RetryQueueItem = {
episode: number;
};
type AnilistPostWatchRunOptions = {
force?: boolean;
};
export function buildAnilistAttemptKey(mediaKey: string, episode: number): string {
return `${mediaKey}::${episode}`;
}
@@ -118,10 +122,11 @@ export function createMaybeRunAnilistPostWatchUpdateHandler(deps: {
minWatchSeconds: number;
minWatchRatio: number;
}) {
return async (): Promise<void> => {
return async (options: AnilistPostWatchRunOptions = {}): Promise<void> => {
if (deps.getInFlight()) {
return;
}
const force = options.force === true;
const resolved = deps.getResolvedConfig();
if (!deps.isAnilistTrackingEnabled(resolved)) {
@@ -129,7 +134,7 @@ export function createMaybeRunAnilistPostWatchUpdateHandler(deps: {
}
const mediaKey = deps.getCurrentMediaKey();
if (!mediaKey || !deps.hasMpvClient()) {
if (!mediaKey || (!force && !deps.hasMpvClient())) {
return;
}
if (isYoutubeMediaPath(mediaKey)) {
@@ -139,17 +144,19 @@ export function createMaybeRunAnilistPostWatchUpdateHandler(deps: {
deps.resetTrackedMedia(mediaKey);
}
const watchedSeconds = deps.getWatchedSeconds();
if (!Number.isFinite(watchedSeconds) || watchedSeconds < deps.minWatchSeconds) {
return;
}
if (!force) {
const watchedSeconds = deps.getWatchedSeconds();
if (!Number.isFinite(watchedSeconds) || watchedSeconds < deps.minWatchSeconds) {
return;
}
const duration = await deps.maybeProbeAnilistDuration(mediaKey);
if (!duration || duration <= 0) {
return;
}
if (watchedSeconds / duration < deps.minWatchRatio) {
return;
const duration = await deps.maybeProbeAnilistDuration(mediaKey);
if (!duration || duration <= 0) {
return;
}
if (watchedSeconds / duration < deps.minWatchRatio) {
return;
}
}
const guess = await deps.ensureAnilistMediaGuess(mediaKey);