mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-20 00:15:27 -07:00
fix(overlay): deduplicate secondary subtitle rendering (#208)
This commit is contained in:
@@ -168,9 +168,40 @@ test('mineSentenceCard prefers a canonical primary subtitle snapshot', async ()
|
||||
]);
|
||||
});
|
||||
|
||||
test('mineSentenceCard refreshes secondary subtitle text before creating card', async () => {
|
||||
test('mineSentenceCard uses normalized secondary subtitle state instead of raw mpv text', async () => {
|
||||
const created: Array<{ sentence: string; secondarySub?: string }> = [];
|
||||
let requestedRawSecondaryText = false;
|
||||
|
||||
await mineSentenceCard({
|
||||
ankiIntegration: {
|
||||
updateLastAddedFromClipboard: async () => {},
|
||||
triggerFieldGroupingForLastAddedCard: async () => {},
|
||||
markLastCardAsAudioCard: async () => {},
|
||||
createSentenceCard: async (sentence, _startTime, _endTime, secondarySub) => {
|
||||
created.push({ sentence, secondarySub });
|
||||
return true;
|
||||
},
|
||||
},
|
||||
mpvClient: {
|
||||
connected: true,
|
||||
currentSubText: '日本語字幕',
|
||||
currentSubStart: 10,
|
||||
currentSubEnd: 12,
|
||||
currentSecondarySubText: 'Your\nmosaic',
|
||||
requestProperty: async () => {
|
||||
requestedRawSecondaryText = true;
|
||||
return 'Your\nYour\nYour\nYour\nmosaic';
|
||||
},
|
||||
},
|
||||
showMpvOsd: () => {},
|
||||
});
|
||||
|
||||
assert.equal(requestedRawSecondaryText, false);
|
||||
assert.deepEqual(created, [{ sentence: '日本語字幕', secondarySub: 'Your\nmosaic' }]);
|
||||
});
|
||||
|
||||
test('mineSentenceCard omits normalized secondary text that matches the primary subtitle', async () => {
|
||||
const created: Array<{ sentence: string; secondarySub?: string }> = [];
|
||||
const requestedProperties: string[] = [];
|
||||
|
||||
await mineSentenceCard({
|
||||
ankiIntegration: {
|
||||
@@ -188,43 +219,6 @@ test('mineSentenceCard refreshes secondary subtitle text before creating card',
|
||||
currentSubStart: 10,
|
||||
currentSubEnd: 12,
|
||||
currentSecondarySubText: '日本語字幕',
|
||||
requestProperty: async (name: string) => {
|
||||
requestedProperties.push(name);
|
||||
return name === 'secondary-sub-text' ? 'English subtitle' : null;
|
||||
},
|
||||
},
|
||||
showMpvOsd: () => {},
|
||||
});
|
||||
|
||||
assert.deepEqual(requestedProperties, ['secondary-sub-text']);
|
||||
assert.deepEqual(created, [{ sentence: '日本語字幕', secondarySub: 'English subtitle' }]);
|
||||
});
|
||||
|
||||
test('mineSentenceCard does not fall back to stale cached secondary subtitle after successful refresh', async () => {
|
||||
const created: Array<{ sentence: string; secondarySub?: string }> = [];
|
||||
|
||||
await mineSentenceCard({
|
||||
ankiIntegration: {
|
||||
updateLastAddedFromClipboard: async () => {},
|
||||
triggerFieldGroupingForLastAddedCard: async () => {},
|
||||
markLastCardAsAudioCard: async () => {},
|
||||
createSentenceCard: async (sentence, _startTime, _endTime, secondarySub) => {
|
||||
created.push({ sentence, secondarySub });
|
||||
return true;
|
||||
},
|
||||
},
|
||||
mpvClient: {
|
||||
connected: true,
|
||||
currentSubText: '日本語字幕',
|
||||
currentSubStart: 10,
|
||||
currentSubEnd: 12,
|
||||
currentSecondarySubText: 'stale cached subtitle',
|
||||
requestProperty: async (name: string) => {
|
||||
if (name === 'secondary-sub-text') {
|
||||
return '';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
showMpvOsd: () => {},
|
||||
});
|
||||
|
||||
@@ -129,18 +129,10 @@ function normalizeSecondarySubText(text: unknown, primaryText: string): string |
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
async function getCurrentSecondarySubTextForSentenceCard(
|
||||
function getCurrentSecondarySubTextForSentenceCard(
|
||||
mpvClient: MpvClientLike,
|
||||
primaryText: string,
|
||||
): Promise<string | undefined> {
|
||||
if (mpvClient.requestProperty) {
|
||||
try {
|
||||
const latestSecondaryText = await mpvClient.requestProperty('secondary-sub-text');
|
||||
return normalizeSecondarySubText(latestSecondaryText, primaryText);
|
||||
} catch {
|
||||
// Fall back to the cached secondary subtitle below.
|
||||
}
|
||||
}
|
||||
): string | undefined {
|
||||
return normalizeSecondarySubText(mpvClient.currentSecondarySubText, primaryText);
|
||||
}
|
||||
|
||||
@@ -192,7 +184,7 @@ export async function mineSentenceCard(deps: {
|
||||
return false;
|
||||
}
|
||||
|
||||
const secondarySubText = await getCurrentSecondarySubTextForSentenceCard(mpvClient, primaryText);
|
||||
const secondarySubText = getCurrentSecondarySubTextForSentenceCard(mpvClient, primaryText);
|
||||
return await anki.createSentenceCard(
|
||||
primaryText,
|
||||
deps.primarySubtitle?.startTime ?? mpvClient.currentSubStart,
|
||||
|
||||
@@ -65,6 +65,8 @@ const MPV_SUBTITLE_PROPERTY_OBSERVATIONS: string[] = [
|
||||
'secondary-sub-visibility',
|
||||
'sub-visibility',
|
||||
'sid',
|
||||
'secondary-sid',
|
||||
'secondary-sub-delay',
|
||||
'track-list',
|
||||
];
|
||||
|
||||
|
||||
@@ -63,6 +63,8 @@ function createDeps(overrides: Partial<MpvProtocolHandleMessageDeps> = {}): {
|
||||
emitSubtitleTiming: (payload) => state.events.push(payload),
|
||||
emitSecondarySubtitleChange: (payload) => state.events.push(payload),
|
||||
emitSubtitleTrackChange: (payload) => state.events.push(payload),
|
||||
emitSecondarySubtitleTrackChange: (payload) => state.events.push(payload),
|
||||
emitSecondarySubtitleDelayChange: (payload) => state.events.push(payload),
|
||||
emitSubtitleTrackListChange: (payload) => state.events.push(payload),
|
||||
getCurrentSubText: () => state.subText,
|
||||
setCurrentSubText: (text) => {
|
||||
@@ -158,12 +160,42 @@ test('dispatchMpvProtocolMessage emits subtitle track changes', async () => {
|
||||
});
|
||||
|
||||
await dispatchMpvProtocolMessage({ event: 'property-change', name: 'sid', data: '3' }, deps);
|
||||
await dispatchMpvProtocolMessage(
|
||||
{ event: 'property-change', name: 'secondary-sid', data: '4' },
|
||||
deps,
|
||||
);
|
||||
await dispatchMpvProtocolMessage(
|
||||
{ event: 'property-change', name: 'secondary-sub-delay', data: '0.5' },
|
||||
deps,
|
||||
);
|
||||
await dispatchMpvProtocolMessage(
|
||||
{ event: 'property-change', name: 'track-list', data: [{ type: 'sub', id: 3 }] },
|
||||
deps,
|
||||
);
|
||||
|
||||
assert.deepEqual(state.events, [{ sid: 3 }, { trackList: [{ type: 'sub', id: 3 }] }]);
|
||||
assert.deepEqual(state.events, [
|
||||
{ sid: 3 },
|
||||
{ sid: 4 },
|
||||
{ delay: 0.5 },
|
||||
{ trackList: [{ type: 'sub', id: 3 }] },
|
||||
]);
|
||||
});
|
||||
|
||||
test('dispatchMpvProtocolMessage rejects decimal subtitle track IDs', async () => {
|
||||
const { deps, state } = createDeps();
|
||||
|
||||
await dispatchMpvProtocolMessage({ event: 'property-change', name: 'sid', data: '4.5' }, deps);
|
||||
await dispatchMpvProtocolMessage(
|
||||
{ event: 'property-change', name: 'secondary-sid', data: '4.5' },
|
||||
deps,
|
||||
);
|
||||
await dispatchMpvProtocolMessage({ event: 'property-change', name: 'sid', data: 4.5 }, deps);
|
||||
await dispatchMpvProtocolMessage(
|
||||
{ event: 'property-change', name: 'secondary-sid', data: 4.5 },
|
||||
deps,
|
||||
);
|
||||
|
||||
assert.deepEqual(state.events, [{ sid: null }, { sid: null }, { sid: null }, { sid: null }]);
|
||||
});
|
||||
|
||||
test('dispatchMpvProtocolMessage enforces sub-visibility hidden when overlay suppression is enabled', async () => {
|
||||
|
||||
@@ -54,6 +54,8 @@ export interface MpvProtocolHandleMessageDeps {
|
||||
emitSubtitleTiming: (payload: { text: string; start: number; end: number }) => void;
|
||||
emitSecondarySubtitleChange: (payload: { text: string }) => void;
|
||||
emitSubtitleTrackChange: (payload: { sid: number | null }) => void;
|
||||
emitSecondarySubtitleTrackChange: (payload: { sid: number | null }) => void;
|
||||
emitSecondarySubtitleDelayChange: (payload: { delay: number }) => void;
|
||||
emitSubtitleTrackListChange: (payload: { trackList: unknown[] | null }) => void;
|
||||
getCurrentSubText: () => string;
|
||||
setCurrentSubText: (text: string) => void;
|
||||
@@ -281,7 +283,25 @@ export async function dispatchMpvProtocolMessage(
|
||||
: typeof msg.data === 'string'
|
||||
? Number(msg.data)
|
||||
: null;
|
||||
deps.emitSubtitleTrackChange({ sid: sid !== null && Number.isFinite(sid) ? sid : null });
|
||||
deps.emitSubtitleTrackChange({ sid: sid !== null && Number.isInteger(sid) ? sid : null });
|
||||
} else if (msg.name === 'secondary-sid') {
|
||||
const sid =
|
||||
typeof msg.data === 'number'
|
||||
? msg.data
|
||||
: typeof msg.data === 'string'
|
||||
? Number(msg.data)
|
||||
: null;
|
||||
deps.emitSecondarySubtitleTrackChange({
|
||||
sid: sid !== null && Number.isInteger(sid) ? sid : null,
|
||||
});
|
||||
} else if (msg.name === 'secondary-sub-delay') {
|
||||
const delay =
|
||||
typeof msg.data === 'number'
|
||||
? msg.data
|
||||
: typeof msg.data === 'string'
|
||||
? Number(msg.data)
|
||||
: 0;
|
||||
deps.emitSecondarySubtitleDelayChange({ delay: Number.isFinite(delay) ? delay : 0 });
|
||||
} else if (msg.name === 'track-list') {
|
||||
deps.emitSubtitleTrackListChange({
|
||||
trackList: Array.isArray(msg.data) ? (msg.data as unknown[]) : null,
|
||||
|
||||
@@ -131,6 +131,8 @@ export interface MpvIpcClientEventMap {
|
||||
'fullscreen-change': { fullscreen: boolean };
|
||||
'secondary-subtitle-change': { text: string };
|
||||
'subtitle-track-change': { sid: number | null };
|
||||
'secondary-subtitle-track-change': { sid: number | null };
|
||||
'secondary-subtitle-delay-change': { delay: number };
|
||||
'subtitle-track-list-change': { trackList: unknown[] | null };
|
||||
'media-path-change': { path: string };
|
||||
'media-title-change': { title: string | null };
|
||||
@@ -438,6 +440,12 @@ export class MpvIpcClient implements MpvClient {
|
||||
emitSubtitleTrackChange: (payload) => {
|
||||
this.emit('subtitle-track-change', payload);
|
||||
},
|
||||
emitSecondarySubtitleTrackChange: (payload) => {
|
||||
this.emit('secondary-subtitle-track-change', payload);
|
||||
},
|
||||
emitSecondarySubtitleDelayChange: (payload) => {
|
||||
this.emit('secondary-subtitle-delay-change', payload);
|
||||
},
|
||||
emitSubtitleTrackListChange: (payload) => {
|
||||
this.emit('subtitle-track-list-change', payload);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user