Fix managed playback exit and tokenizer grammar splits

- Ignore background stats daemons during regular app startup
- Split standalone grammar endings before applying annotations
- Clear helper-span annotations for auxiliary-only tokens
This commit is contained in:
2026-05-02 18:36:41 -07:00
parent 2a06bfc989
commit 6607b06437
12 changed files with 583 additions and 88 deletions
@@ -162,6 +162,48 @@ test('mpv main event main deps wire subtitle callbacks without suppression gate'
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({
+11 -3
View File
@@ -2,7 +2,11 @@ import type { MergedToken, SubtitleData } from '../../types';
export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
appState: {
initialArgs?: { jellyfinPlay?: unknown; youtubePlay?: unknown } | null;
initialArgs?: {
jellyfinPlay?: unknown;
managedPlayback?: unknown;
youtubePlay?: unknown;
} | null;
overlayRuntimeInitialized: boolean;
mpvClient: {
connected?: boolean;
@@ -79,10 +83,14 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
syncOverlayMpvSubtitleSuppression: () => deps.syncOverlayMpvSubtitleSuppression(),
hasInitialPlaybackQuitOnDisconnectArg: () =>
Boolean(deps.appState.initialArgs?.jellyfinPlay || deps.appState.initialArgs?.youtubePlay),
Boolean(
deps.appState.initialArgs?.managedPlayback ||
deps.appState.initialArgs?.jellyfinPlay ||
deps.appState.initialArgs?.youtubePlay,
),
isOverlayRuntimeInitialized: () => deps.appState.overlayRuntimeInitialized,
shouldQuitOnDisconnectWhenOverlayRuntimeInitialized: () =>
Boolean(deps.appState.initialArgs?.youtubePlay),
Boolean(deps.appState.initialArgs?.managedPlayback || deps.appState.initialArgs?.youtubePlay),
isQuitOnDisconnectArmed: () => deps.getQuitOnDisconnectArmed(),
scheduleQuitCheck: (callback: () => void) => deps.scheduleQuitCheck(callback),
isMpvConnected: () => Boolean(deps.appState.mpvClient?.connected),
@@ -36,14 +36,14 @@ function createHarness(options?: {
};
}
test('stats server routing defers to a live background daemon from another process', () => {
test('stats server routing ignores a live background daemon from another process', () => {
const { calls, handler } = createHarness({
state: { pid: 200, port: 7979, startedAtMs: 1 },
processAlive: true,
});
assert.deepEqual(handler(), { url: 'http://127.0.0.1:7979', source: 'foreign' });
assert.deepEqual(calls, ['readBackgroundState', 'isProcessAlive']);
assert.deepEqual(handler(), { url: 'http://127.0.0.1:6969', source: 'local' });
assert.deepEqual(calls, ['readBackgroundState', 'isProcessAlive', 'startLocalStatsServer']);
});
test('stats server routing clears dead daemon state and starts local server', () => {
+1 -5
View File
@@ -14,9 +14,7 @@ function formatStatsServerUrl(port: number): string {
return `http://127.0.0.1:${port}`;
}
export type EnsureStatsServerUrlResult =
| { url: string; source: 'foreign' }
| { url: string; source: 'local' };
export type EnsureStatsServerUrlResult = { url: string; source: 'local' };
export function createEnsureStatsServerUrlHandler(
deps: EnsureStatsServerUrlDeps,
@@ -29,8 +27,6 @@ export function createEnsureStatsServerUrlHandler(
deps.removeBackgroundState();
} else if (!deps.isProcessAlive(state.pid)) {
deps.removeBackgroundState();
} else if (state.pid !== deps.currentPid) {
return { url: formatStatsServerUrl(state.port), source: 'foreign' };
}
if (!deps.hasLocalStatsServer()) {