mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 00:55:16 -07:00
403ee32579
- Remove --background from launcher-owned mpv starts; quit only non-tray/non-background managed sessions - Defer autoplay-ready signal until overlay window content is loaded; retry after flush - Retry socket availability before auto-starting overlay (up to 25 attempts, 200ms apart) - Extract warm tokenization signal into autoplay-tokenization-warm-release with stale-media guard - Queue second-instance commands until app ready runtime completes - Guard globalShortcut cleanup with isAppReady check to avoid pre-ready crash - Recognize "osx" as a macOS platform alias in Lua environment detection
267 lines
11 KiB
TypeScript
267 lines
11 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { createBuildBindMpvMainEventHandlersMainDepsHandler } from './mpv-main-event-main-deps';
|
|
|
|
test('mpv main event main deps map app state updates and delegate callbacks', async () => {
|
|
const calls: string[] = [];
|
|
const appState = {
|
|
initialArgs: { jellyfinPlay: true },
|
|
overlayRuntimeInitialized: true,
|
|
mpvClient: {
|
|
connected: true,
|
|
currentTimePos: 12.25,
|
|
requestProperty: async () => 18.75,
|
|
},
|
|
immersionTracker: {
|
|
recordSubtitleLine: (text: string) => calls.push(`immersion-sub:${text}`),
|
|
handleMediaTitleUpdate: (title: string) => calls.push(`immersion-title:${title}`),
|
|
recordPlaybackPosition: (time: number) => calls.push(`immersion-time:${time}`),
|
|
recordMediaDuration: (durationSec: number) => calls.push(`immersion-duration:${durationSec}`),
|
|
recordPauseState: (paused: boolean) => calls.push(`immersion-pause:${paused}`),
|
|
},
|
|
subtitleTimingTracker: {
|
|
recordSubtitle: (text: string) => calls.push(`timing:${text}`),
|
|
},
|
|
currentSubText: '',
|
|
currentSubAssText: '',
|
|
playbackPaused: null,
|
|
previousSecondarySubVisibility: false,
|
|
};
|
|
|
|
const deps = createBuildBindMpvMainEventHandlersMainDepsHandler({
|
|
appState,
|
|
getQuitOnDisconnectArmed: () => true,
|
|
scheduleQuitCheck: (callback) => {
|
|
calls.push('schedule');
|
|
callback();
|
|
},
|
|
quitApp: () => calls.push('quit'),
|
|
reportJellyfinRemoteStopped: () => calls.push('remote-stopped'),
|
|
syncOverlayMpvSubtitleSuppression: () => calls.push('sync-overlay-mpv-sub'),
|
|
maybeRunAnilistPostWatchUpdate: async () => {
|
|
calls.push('anilist-post-watch');
|
|
},
|
|
recordAnilistMediaDuration: (durationSec) => calls.push(`anilist-duration:${durationSec}`),
|
|
logSubtitleTimingError: (message) => calls.push(`subtitle-error:${message}`),
|
|
broadcastToOverlayWindows: (channel, payload) =>
|
|
calls.push(`broadcast:${channel}:${String(payload)}`),
|
|
onSubtitleChange: (text) => calls.push(`subtitle-change:${text}`),
|
|
ensureImmersionTrackerInitialized: () => calls.push('ensure-immersion'),
|
|
updateCurrentMediaPath: (path) => calls.push(`path:${path}`),
|
|
restoreMpvSubVisibility: () => calls.push('restore-mpv-sub'),
|
|
resetSubtitleSidebarEmbeddedLayout: () => calls.push('reset-sidebar-layout'),
|
|
getCurrentAnilistMediaKey: () => 'media-key',
|
|
resetAnilistMediaTracking: (mediaKey) => calls.push(`reset:${mediaKey}`),
|
|
maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${mediaKey}`),
|
|
ensureAnilistMediaGuess: (mediaKey) => calls.push(`guess:${mediaKey}`),
|
|
syncImmersionMediaState: () => calls.push('sync-immersion'),
|
|
signalAutoplayReadyIfWarm: (path) => calls.push(`autoplay:${path}`),
|
|
updateCurrentMediaTitle: (title) => calls.push(`title:${title}`),
|
|
resetAnilistMediaGuessState: () => calls.push('reset-guess'),
|
|
reportJellyfinRemoteProgress: (forceImmediate) => calls.push(`progress:${forceImmediate}`),
|
|
onFullscreenChange: (fullscreen) => calls.push(`fullscreen:${fullscreen}`),
|
|
updateSubtitleRenderMetrics: () => calls.push('metrics'),
|
|
refreshDiscordPresence: () => calls.push('presence-refresh'),
|
|
})();
|
|
|
|
assert.equal(deps.hasInitialPlaybackQuitOnDisconnectArg(), true);
|
|
assert.equal(deps.isOverlayRuntimeInitialized(), true);
|
|
assert.equal(deps.isQuitOnDisconnectArmed(), true);
|
|
assert.equal(deps.isMpvConnected(), true);
|
|
deps.scheduleQuitCheck(() => calls.push('scheduled-callback'));
|
|
deps.quitApp();
|
|
deps.reportJellyfinRemoteStopped();
|
|
deps.syncOverlayMpvSubtitleSuppression();
|
|
deps.recordImmersionSubtitleLine('x', 0, 1);
|
|
assert.equal(deps.hasSubtitleTimingTracker(), true);
|
|
deps.recordSubtitleTiming('y', 0, 1);
|
|
await deps.maybeRunAnilistPostWatchUpdate();
|
|
deps.logSubtitleTimingError('err', new Error('boom'));
|
|
deps.setCurrentSubText('sub');
|
|
deps.broadcastSubtitle({ text: 'sub', tokens: null });
|
|
deps.onSubtitleChange('sub');
|
|
deps.refreshDiscordPresence();
|
|
deps.setCurrentSubAssText('ass');
|
|
deps.broadcastSubtitleAss('ass');
|
|
deps.broadcastSecondarySubtitle('sec');
|
|
deps.updateCurrentMediaPath('/tmp/video');
|
|
deps.restoreMpvSubVisibility();
|
|
deps.resetSubtitleSidebarEmbeddedLayout();
|
|
assert.equal(deps.getCurrentAnilistMediaKey(), 'media-key');
|
|
deps.resetAnilistMediaTracking('media-key');
|
|
deps.maybeProbeAnilistDuration('media-key');
|
|
deps.ensureAnilistMediaGuess('media-key');
|
|
deps.syncImmersionMediaState();
|
|
deps.signalAutoplayReadyIfWarm?.('/tmp/video');
|
|
deps.updateCurrentMediaTitle('title');
|
|
deps.resetAnilistMediaGuessState();
|
|
deps.notifyImmersionTitleUpdate('title');
|
|
deps.recordPlaybackPosition(10);
|
|
deps.recordMediaDuration(1234);
|
|
deps.reportJellyfinRemoteProgress(true);
|
|
deps.onFullscreenChange?.(true);
|
|
deps.recordPauseState(true);
|
|
deps.updateSubtitleRenderMetrics({});
|
|
deps.setPreviousSecondarySubVisibility(true);
|
|
deps.flushPlaybackPositionOnMediaPathClear?.('');
|
|
await Promise.resolve();
|
|
|
|
assert.equal(appState.currentSubText, 'sub');
|
|
assert.equal(appState.currentSubAssText, 'ass');
|
|
assert.equal(appState.playbackPaused, true);
|
|
assert.equal(appState.previousSecondarySubVisibility, true);
|
|
assert.ok(calls.includes('remote-stopped'));
|
|
assert.ok(calls.includes('sync-overlay-mpv-sub'));
|
|
assert.ok(calls.includes('anilist-post-watch'));
|
|
assert.ok(calls.includes('ensure-immersion'));
|
|
assert.ok(calls.includes('sync-immersion'));
|
|
assert.ok(calls.includes('autoplay:/tmp/video'));
|
|
assert.ok(calls.includes('metrics'));
|
|
assert.ok(calls.includes('fullscreen:true'));
|
|
assert.ok(calls.includes('presence-refresh'));
|
|
assert.ok(calls.includes('restore-mpv-sub'));
|
|
assert.ok(calls.includes('reset-sidebar-layout'));
|
|
assert.ok(calls.includes('immersion-duration:1234'));
|
|
assert.ok(calls.includes('anilist-duration:1234'));
|
|
});
|
|
|
|
test('mpv main event main deps wire subtitle callbacks without suppression gate', () => {
|
|
const deps = createBuildBindMpvMainEventHandlersMainDepsHandler({
|
|
appState: {
|
|
initialArgs: null,
|
|
overlayRuntimeInitialized: true,
|
|
mpvClient: null,
|
|
immersionTracker: null,
|
|
subtitleTimingTracker: null,
|
|
currentSubText: '',
|
|
currentSubAssText: '',
|
|
playbackPaused: null,
|
|
previousSecondarySubVisibility: false,
|
|
},
|
|
getQuitOnDisconnectArmed: () => false,
|
|
scheduleQuitCheck: () => {},
|
|
quitApp: () => {},
|
|
reportJellyfinRemoteStopped: () => {},
|
|
syncOverlayMpvSubtitleSuppression: () => {},
|
|
maybeRunAnilistPostWatchUpdate: async () => {},
|
|
logSubtitleTimingError: () => {},
|
|
broadcastToOverlayWindows: () => {},
|
|
onSubtitleChange: () => {},
|
|
ensureImmersionTrackerInitialized: () => {},
|
|
updateCurrentMediaPath: () => {},
|
|
restoreMpvSubVisibility: () => {},
|
|
resetSubtitleSidebarEmbeddedLayout: () => {},
|
|
getCurrentAnilistMediaKey: () => null,
|
|
resetAnilistMediaTracking: () => {},
|
|
maybeProbeAnilistDuration: () => {},
|
|
ensureAnilistMediaGuess: () => {},
|
|
syncImmersionMediaState: () => {},
|
|
updateCurrentMediaTitle: () => {},
|
|
resetAnilistMediaGuessState: () => {},
|
|
reportJellyfinRemoteProgress: () => {},
|
|
updateSubtitleRenderMetrics: () => {},
|
|
refreshDiscordPresence: () => {},
|
|
})();
|
|
|
|
deps.setCurrentSubText('sub');
|
|
assert.equal(typeof deps.setCurrentSubText, 'function');
|
|
});
|
|
|
|
test('mpv main event main deps treat managed playback as quit-on-disconnect', () => {
|
|
const deps = createBuildBindMpvMainEventHandlersMainDepsHandler({
|
|
appState: {
|
|
initialArgs: { managedPlayback: true },
|
|
overlayRuntimeInitialized: false,
|
|
mpvClient: null,
|
|
immersionTracker: null,
|
|
subtitleTimingTracker: null,
|
|
currentSubText: '',
|
|
currentSubAssText: '',
|
|
playbackPaused: null,
|
|
previousSecondarySubVisibility: false,
|
|
},
|
|
getQuitOnDisconnectArmed: () => true,
|
|
scheduleQuitCheck: () => {},
|
|
quitApp: () => {},
|
|
reportJellyfinRemoteStopped: () => {},
|
|
syncOverlayMpvSubtitleSuppression: () => {},
|
|
maybeRunAnilistPostWatchUpdate: async () => {},
|
|
logSubtitleTimingError: () => {},
|
|
broadcastToOverlayWindows: () => {},
|
|
onSubtitleChange: () => {},
|
|
ensureImmersionTrackerInitialized: () => {},
|
|
updateCurrentMediaPath: () => {},
|
|
restoreMpvSubVisibility: () => {},
|
|
resetSubtitleSidebarEmbeddedLayout: () => {},
|
|
getCurrentAnilistMediaKey: () => null,
|
|
resetAnilistMediaTracking: () => {},
|
|
maybeProbeAnilistDuration: () => {},
|
|
ensureAnilistMediaGuess: () => {},
|
|
syncImmersionMediaState: () => {},
|
|
updateCurrentMediaTitle: () => {},
|
|
resetAnilistMediaGuessState: () => {},
|
|
reportJellyfinRemoteProgress: () => {},
|
|
updateSubtitleRenderMetrics: () => {},
|
|
refreshDiscordPresence: () => {},
|
|
})();
|
|
|
|
assert.equal(deps.hasInitialPlaybackQuitOnDisconnectArg(), true);
|
|
assert.equal(deps.shouldQuitOnDisconnectWhenOverlayRuntimeInitialized(), true);
|
|
});
|
|
|
|
test('flushPlaybackPositionOnMediaPathClear ignores disconnected mpv time-pos reads', async () => {
|
|
const recorded: number[] = [];
|
|
const deps = createBuildBindMpvMainEventHandlersMainDepsHandler({
|
|
appState: {
|
|
initialArgs: null,
|
|
overlayRuntimeInitialized: true,
|
|
mpvClient: {
|
|
connected: false,
|
|
currentTimePos: 42,
|
|
requestProperty: async () => {
|
|
throw new Error('disconnected');
|
|
},
|
|
},
|
|
immersionTracker: {
|
|
recordPlaybackPosition: (time: number) => {
|
|
recorded.push(time);
|
|
},
|
|
},
|
|
subtitleTimingTracker: null,
|
|
currentMediaPath: '',
|
|
currentSubText: '',
|
|
currentSubAssText: '',
|
|
playbackPaused: null,
|
|
previousSecondarySubVisibility: false,
|
|
},
|
|
getQuitOnDisconnectArmed: () => false,
|
|
scheduleQuitCheck: () => {},
|
|
quitApp: () => {},
|
|
reportJellyfinRemoteStopped: () => {},
|
|
syncOverlayMpvSubtitleSuppression: () => {},
|
|
maybeRunAnilistPostWatchUpdate: async () => {},
|
|
logSubtitleTimingError: () => {},
|
|
broadcastToOverlayWindows: () => {},
|
|
onSubtitleChange: () => {},
|
|
ensureImmersionTrackerInitialized: () => {},
|
|
updateCurrentMediaPath: () => {},
|
|
restoreMpvSubVisibility: () => {},
|
|
resetSubtitleSidebarEmbeddedLayout: () => {},
|
|
getCurrentAnilistMediaKey: () => null,
|
|
resetAnilistMediaTracking: () => {},
|
|
maybeProbeAnilistDuration: () => {},
|
|
ensureAnilistMediaGuess: () => {},
|
|
syncImmersionMediaState: () => {},
|
|
updateCurrentMediaTitle: () => {},
|
|
resetAnilistMediaGuessState: () => {},
|
|
reportJellyfinRemoteProgress: () => {},
|
|
updateSubtitleRenderMetrics: () => {},
|
|
refreshDiscordPresence: () => {},
|
|
})();
|
|
|
|
deps.flushPlaybackPositionOnMediaPathClear?.('');
|
|
await Promise.resolve();
|
|
|
|
assert.deepEqual(recorded, [42]);
|
|
});
|