mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 12:11:28 -07:00
fix: flush playback position before media path clear
This commit is contained in:
@@ -87,6 +87,7 @@ test('media path change handler reports stop for empty path and probes media key
|
|||||||
maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${mediaKey}`),
|
maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${mediaKey}`),
|
||||||
ensureAnilistMediaGuess: (mediaKey) => calls.push(`guess:${mediaKey}`),
|
ensureAnilistMediaGuess: (mediaKey) => calls.push(`guess:${mediaKey}`),
|
||||||
syncImmersionMediaState: () => calls.push('sync'),
|
syncImmersionMediaState: () => calls.push('sync'),
|
||||||
|
flushPlaybackPositionOnMediaPathClear: () => calls.push('flush-playback'),
|
||||||
scheduleCharacterDictionarySync: () => calls.push('dict-sync'),
|
scheduleCharacterDictionarySync: () => calls.push('dict-sync'),
|
||||||
signalAutoplayReadyIfWarm: (path) => calls.push(`autoplay:${path}`),
|
signalAutoplayReadyIfWarm: (path) => calls.push(`autoplay:${path}`),
|
||||||
refreshDiscordPresence: () => calls.push('presence'),
|
refreshDiscordPresence: () => calls.push('presence'),
|
||||||
@@ -94,6 +95,7 @@ test('media path change handler reports stop for empty path and probes media key
|
|||||||
|
|
||||||
handler({ path: '' });
|
handler({ path: '' });
|
||||||
assert.deepEqual(calls, [
|
assert.deepEqual(calls, [
|
||||||
|
'flush-playback',
|
||||||
'path:',
|
'path:',
|
||||||
'stopped',
|
'stopped',
|
||||||
'restore-mpv-sub',
|
'restore-mpv-sub',
|
||||||
@@ -116,6 +118,7 @@ test('media path change handler signals autoplay-ready fast path for warm non-em
|
|||||||
maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${mediaKey}`),
|
maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${mediaKey}`),
|
||||||
ensureAnilistMediaGuess: (mediaKey) => calls.push(`guess:${mediaKey}`),
|
ensureAnilistMediaGuess: (mediaKey) => calls.push(`guess:${mediaKey}`),
|
||||||
syncImmersionMediaState: () => calls.push('sync'),
|
syncImmersionMediaState: () => calls.push('sync'),
|
||||||
|
flushPlaybackPositionOnMediaPathClear: () => calls.push('flush-playback'),
|
||||||
scheduleCharacterDictionarySync: () => calls.push('dict-sync'),
|
scheduleCharacterDictionarySync: () => calls.push('dict-sync'),
|
||||||
signalAutoplayReadyIfWarm: (path) => calls.push(`autoplay:${path}`),
|
signalAutoplayReadyIfWarm: (path) => calls.push(`autoplay:${path}`),
|
||||||
refreshDiscordPresence: () => calls.push('presence'),
|
refreshDiscordPresence: () => calls.push('presence'),
|
||||||
@@ -133,6 +136,35 @@ test('media path change handler signals autoplay-ready fast path for warm non-em
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('media path change handler ignores playback flush for non-empty path', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const handler = createHandleMpvMediaPathChangeHandler({
|
||||||
|
updateCurrentMediaPath: (path) => calls.push(`path:${path}`),
|
||||||
|
reportJellyfinRemoteStopped: () => calls.push('stopped'),
|
||||||
|
restoreMpvSubVisibility: () => calls.push('restore-mpv-sub'),
|
||||||
|
getCurrentAnilistMediaKey: () => null,
|
||||||
|
resetAnilistMediaTracking: (mediaKey) => calls.push(`reset:${String(mediaKey)}`),
|
||||||
|
maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${mediaKey}`),
|
||||||
|
ensureAnilistMediaGuess: (mediaKey) => calls.push(`guess:${mediaKey}`),
|
||||||
|
syncImmersionMediaState: () => calls.push('sync'),
|
||||||
|
flushPlaybackPositionOnMediaPathClear: () => calls.push('flush-playback'),
|
||||||
|
scheduleCharacterDictionarySync: () => calls.push('dict-sync'),
|
||||||
|
signalAutoplayReadyIfWarm: (path) => calls.push(`autoplay:${path}`),
|
||||||
|
refreshDiscordPresence: () => calls.push('presence'),
|
||||||
|
});
|
||||||
|
|
||||||
|
handler({ path: '/tmp/video.mkv' });
|
||||||
|
assert.ok(!calls.includes('flush-playback'));
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
'path:/tmp/video.mkv',
|
||||||
|
'reset:null',
|
||||||
|
'sync',
|
||||||
|
'dict-sync',
|
||||||
|
'autoplay:/tmp/video.mkv',
|
||||||
|
'presence',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test('media title change handler clears guess state without re-scheduling character dictionary sync', () => {
|
test('media title change handler clears guess state without re-scheduling character dictionary sync', () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const deps: Parameters<typeof createHandleMpvMediaTitleChangeHandler>[0] & {
|
const deps: Parameters<typeof createHandleMpvMediaTitleChangeHandler>[0] & {
|
||||||
|
|||||||
@@ -53,10 +53,14 @@ export function createHandleMpvMediaPathChangeHandler(deps: {
|
|||||||
syncImmersionMediaState: () => void;
|
syncImmersionMediaState: () => void;
|
||||||
scheduleCharacterDictionarySync?: () => void;
|
scheduleCharacterDictionarySync?: () => void;
|
||||||
signalAutoplayReadyIfWarm?: (path: string) => void;
|
signalAutoplayReadyIfWarm?: (path: string) => void;
|
||||||
|
flushPlaybackPositionOnMediaPathClear?: (mediaPath: string) => void;
|
||||||
refreshDiscordPresence: () => void;
|
refreshDiscordPresence: () => void;
|
||||||
}) {
|
}) {
|
||||||
return ({ path }: { path: string | null }): void => {
|
return ({ path }: { path: string | null }): void => {
|
||||||
const normalizedPath = typeof path === 'string' ? path : '';
|
const normalizedPath = typeof path === 'string' ? path : '';
|
||||||
|
if (!normalizedPath) {
|
||||||
|
deps.flushPlaybackPositionOnMediaPathClear?.(normalizedPath);
|
||||||
|
}
|
||||||
deps.updateCurrentMediaPath(normalizedPath);
|
deps.updateCurrentMediaPath(normalizedPath);
|
||||||
if (!normalizedPath) {
|
if (!normalizedPath) {
|
||||||
deps.reportJellyfinRemoteStopped();
|
deps.reportJellyfinRemoteStopped();
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ test('main mpv event binder wires callbacks through to runtime deps', () => {
|
|||||||
maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${mediaKey}`),
|
maybeProbeAnilistDuration: (mediaKey) => calls.push(`probe:${mediaKey}`),
|
||||||
ensureAnilistMediaGuess: (mediaKey) => calls.push(`guess:${mediaKey}`),
|
ensureAnilistMediaGuess: (mediaKey) => calls.push(`guess:${mediaKey}`),
|
||||||
syncImmersionMediaState: () => calls.push('sync-immersion'),
|
syncImmersionMediaState: () => calls.push('sync-immersion'),
|
||||||
|
flushPlaybackPositionOnMediaPathClear: () => calls.push('flush-playback'),
|
||||||
|
|
||||||
updateCurrentMediaTitle: (title) => calls.push(`media-title:${title}`),
|
updateCurrentMediaTitle: (title) => calls.push(`media-title:${title}`),
|
||||||
resetAnilistMediaGuessState: () => calls.push('reset-guess-state'),
|
resetAnilistMediaGuessState: () => calls.push('reset-guess-state'),
|
||||||
@@ -86,4 +87,6 @@ test('main mpv event binder wires callbacks through to runtime deps', () => {
|
|||||||
assert.ok(calls.includes('progress:normal'));
|
assert.ok(calls.includes('progress:normal'));
|
||||||
assert.ok(calls.includes('progress:force'));
|
assert.ok(calls.includes('progress:force'));
|
||||||
assert.ok(calls.includes('presence-refresh'));
|
assert.ok(calls.includes('presence-refresh'));
|
||||||
|
assert.ok(calls.includes('sync-immersion'));
|
||||||
|
assert.ok(calls.includes('flush-playback'));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export function createBindMpvMainEventHandlersHandler(deps: {
|
|||||||
ensureAnilistMediaGuess: (mediaKey: string) => void;
|
ensureAnilistMediaGuess: (mediaKey: string) => void;
|
||||||
syncImmersionMediaState: () => void;
|
syncImmersionMediaState: () => void;
|
||||||
signalAutoplayReadyIfWarm?: (path: string) => void;
|
signalAutoplayReadyIfWarm?: (path: string) => void;
|
||||||
|
flushPlaybackPositionOnMediaPathClear?: (mediaPath: string) => void;
|
||||||
|
|
||||||
updateCurrentMediaTitle: (title: string) => void;
|
updateCurrentMediaTitle: (title: string) => void;
|
||||||
resetAnilistMediaGuessState: () => void;
|
resetAnilistMediaGuessState: () => void;
|
||||||
@@ -114,6 +115,8 @@ export function createBindMpvMainEventHandlersHandler(deps: {
|
|||||||
maybeProbeAnilistDuration: (mediaKey) => deps.maybeProbeAnilistDuration(mediaKey),
|
maybeProbeAnilistDuration: (mediaKey) => deps.maybeProbeAnilistDuration(mediaKey),
|
||||||
ensureAnilistMediaGuess: (mediaKey) => deps.ensureAnilistMediaGuess(mediaKey),
|
ensureAnilistMediaGuess: (mediaKey) => deps.ensureAnilistMediaGuess(mediaKey),
|
||||||
syncImmersionMediaState: () => deps.syncImmersionMediaState(),
|
syncImmersionMediaState: () => deps.syncImmersionMediaState(),
|
||||||
|
flushPlaybackPositionOnMediaPathClear: (mediaPath) =>
|
||||||
|
deps.flushPlaybackPositionOnMediaPathClear?.(mediaPath),
|
||||||
signalAutoplayReadyIfWarm: (path) => deps.signalAutoplayReadyIfWarm?.(path),
|
signalAutoplayReadyIfWarm: (path) => deps.signalAutoplayReadyIfWarm?.(path),
|
||||||
scheduleCharacterDictionarySync: () => deps.scheduleCharacterDictionarySync?.(),
|
scheduleCharacterDictionarySync: () => deps.scheduleCharacterDictionarySync?.(),
|
||||||
refreshDiscordPresence: () => deps.refreshDiscordPresence(),
|
refreshDiscordPresence: () => deps.refreshDiscordPresence(),
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
|
|||||||
const appState = {
|
const appState = {
|
||||||
initialArgs: { jellyfinPlay: true },
|
initialArgs: { jellyfinPlay: true },
|
||||||
overlayRuntimeInitialized: true,
|
overlayRuntimeInitialized: true,
|
||||||
mpvClient: { connected: true },
|
mpvClient: {
|
||||||
|
connected: true,
|
||||||
|
currentTimePos: 12.25,
|
||||||
|
requestProperty: async () => 18.75,
|
||||||
|
},
|
||||||
immersionTracker: {
|
immersionTracker: {
|
||||||
recordSubtitleLine: (text: string) => calls.push(`immersion-sub:${text}`),
|
recordSubtitleLine: (text: string) => calls.push(`immersion-sub:${text}`),
|
||||||
handleMediaTitleUpdate: (title: string) => calls.push(`immersion-title:${title}`),
|
handleMediaTitleUpdate: (title: string) => calls.push(`immersion-title:${title}`),
|
||||||
@@ -92,6 +96,8 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
|
|||||||
deps.recordPauseState(true);
|
deps.recordPauseState(true);
|
||||||
deps.updateSubtitleRenderMetrics({});
|
deps.updateSubtitleRenderMetrics({});
|
||||||
deps.setPreviousSecondarySubVisibility(true);
|
deps.setPreviousSecondarySubVisibility(true);
|
||||||
|
deps.flushPlaybackPositionOnMediaPathClear?.('');
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
assert.equal(appState.currentSubText, 'sub');
|
assert.equal(appState.currentSubText, 'sub');
|
||||||
assert.equal(appState.currentSubAssText, 'ass');
|
assert.equal(appState.currentSubAssText, 'ass');
|
||||||
@@ -106,4 +112,6 @@ test('mpv main event main deps map app state updates and delegate callbacks', as
|
|||||||
assert.ok(calls.includes('metrics'));
|
assert.ok(calls.includes('metrics'));
|
||||||
assert.ok(calls.includes('presence-refresh'));
|
assert.ok(calls.includes('presence-refresh'));
|
||||||
assert.ok(calls.includes('restore-mpv-sub'));
|
assert.ok(calls.includes('restore-mpv-sub'));
|
||||||
|
assert.ok(calls.includes('immersion-time:12.25'));
|
||||||
|
assert.ok(calls.includes('immersion-time:18.75'));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,14 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
|||||||
appState: {
|
appState: {
|
||||||
initialArgs?: { jellyfinPlay?: unknown } | null;
|
initialArgs?: { jellyfinPlay?: unknown } | null;
|
||||||
overlayRuntimeInitialized: boolean;
|
overlayRuntimeInitialized: boolean;
|
||||||
mpvClient: { connected?: boolean; currentSecondarySubText?: string } | null;
|
mpvClient:
|
||||||
|
| {
|
||||||
|
connected?: boolean;
|
||||||
|
currentSecondarySubText?: string;
|
||||||
|
currentTimePos?: number;
|
||||||
|
requestProperty?: (name: string) => Promise<unknown>;
|
||||||
|
}
|
||||||
|
| null;
|
||||||
immersionTracker: {
|
immersionTracker: {
|
||||||
recordSubtitleLine?: (
|
recordSubtitleLine?: (
|
||||||
text: string,
|
text: string,
|
||||||
@@ -21,6 +28,7 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
|||||||
subtitleTimingTracker: {
|
subtitleTimingTracker: {
|
||||||
recordSubtitle?: (text: string, start: number, end: number) => void;
|
recordSubtitle?: (text: string, start: number, end: number) => void;
|
||||||
} | null;
|
} | null;
|
||||||
|
currentMediaPath?: string | null;
|
||||||
currentSubText: string;
|
currentSubText: string;
|
||||||
currentSubAssText: string;
|
currentSubAssText: string;
|
||||||
currentSubtitleData?: SubtitleData | null;
|
currentSubtitleData?: SubtitleData | null;
|
||||||
@@ -58,6 +66,15 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
|||||||
ensureImmersionTrackerInitialized: () => void;
|
ensureImmersionTrackerInitialized: () => void;
|
||||||
tokenizeSubtitleForImmersion?: (text: string) => Promise<SubtitleData | null>;
|
tokenizeSubtitleForImmersion?: (text: string) => Promise<SubtitleData | null>;
|
||||||
}) {
|
}) {
|
||||||
|
const writePlaybackPositionFromMpv = (timeSec: unknown): void => {
|
||||||
|
const normalizedTimeSec = Number(timeSec);
|
||||||
|
if (!Number.isFinite(normalizedTimeSec)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deps.ensureImmersionTrackerInitialized();
|
||||||
|
deps.appState.immersionTracker?.recordPlaybackPosition?.(normalizedTimeSec);
|
||||||
|
};
|
||||||
|
|
||||||
return () => ({
|
return () => ({
|
||||||
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
|
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
|
||||||
syncOverlayMpvSubtitleSuppression: () => deps.syncOverlayMpvSubtitleSuppression(),
|
syncOverlayMpvSubtitleSuppression: () => deps.syncOverlayMpvSubtitleSuppression(),
|
||||||
@@ -161,6 +178,25 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
|
|||||||
deps.ensureImmersionTrackerInitialized();
|
deps.ensureImmersionTrackerInitialized();
|
||||||
deps.appState.immersionTracker?.recordPauseState?.(paused);
|
deps.appState.immersionTracker?.recordPauseState?.(paused);
|
||||||
},
|
},
|
||||||
|
flushPlaybackPositionOnMediaPathClear: (mediaPath: string) => {
|
||||||
|
const mpvClient = deps.appState.mpvClient;
|
||||||
|
const currentKnownTime = Number(mpvClient?.currentTimePos);
|
||||||
|
writePlaybackPositionFromMpv(currentKnownTime);
|
||||||
|
if (!mpvClient?.requestProperty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void mpvClient.requestProperty('time-pos').then((timePos) => {
|
||||||
|
const currentPath = (deps.appState.currentMediaPath ?? '').trim();
|
||||||
|
if (currentPath.length > 0 && currentPath !== mediaPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const resolvedTime = Number(timePos);
|
||||||
|
if (Number.isFinite(currentKnownTime) && Number.isFinite(resolvedTime) && currentKnownTime === resolvedTime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
writePlaybackPositionFromMpv(resolvedTime);
|
||||||
|
});
|
||||||
|
},
|
||||||
updateSubtitleRenderMetrics: (patch: Record<string, unknown>) =>
|
updateSubtitleRenderMetrics: (patch: Record<string, unknown>) =>
|
||||||
deps.updateSubtitleRenderMetrics(patch),
|
deps.updateSubtitleRenderMetrics(patch),
|
||||||
setPreviousSecondarySubVisibility: (visible: boolean) => {
|
setPreviousSecondarySubVisibility: (visible: boolean) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user