fix: address coderabbit feedback

This commit is contained in:
2026-05-03 21:07:02 -07:00
parent 12e1e783c9
commit 837f21b346
13 changed files with 207 additions and 21 deletions
@@ -141,3 +141,37 @@ test('manual clipboard subtitle update replaces sentence audio without touching
[true],
);
});
test('manual clipboard subtitle update skips audio when sentence audio field is missing', async () => {
const { service, updatedFields, mergeCalls, storedMedia } = createManualUpdateService({
client: {
addNote: async () => 0,
addTags: async () => undefined,
notesInfo: async () => [
{
noteId: 42,
fields: {
Expression: { value: '単語' },
Sentence: { value: '' },
ExpressionAudio: { value: '[sound:auto-expression.mp3]' },
},
},
],
updateNoteFields: async (_noteId, fields) => {
updatedFields.push(fields);
},
storeMediaFile: async (filename) => {
storedMedia.push(filename);
},
findNotes: async () => [42],
retrieveMediaFile: async () => '',
},
});
await service.updateLastAddedFromClipboard('字幕');
assert.equal(storedMedia.length, 1);
assert.equal(updatedFields.length, 1);
assert.deepEqual(updatedFields[0], { Sentence: '字幕' });
assert.equal(mergeCalls.length, 0);
});
+8 -1
View File
@@ -218,7 +218,7 @@ export class CardCreationService {
fields,
this.deps.getConfig(),
);
const sentenceAudioField = this.getResolvedSentenceAudioFieldName(noteInfo);
const sentenceAudioField = this.getResolvedSentenceOnlyAudioFieldName(noteInfo);
const sentenceField = this.deps.getEffectiveSentenceCardConfig().sentenceField;
const sentence = blocks.join(' ');
@@ -721,6 +721,13 @@ export class CardCreationService {
);
}
private getResolvedSentenceOnlyAudioFieldName(noteInfo: CardCreationNoteInfo): string | null {
return this.deps.resolveNoteFieldName(
noteInfo,
this.deps.getEffectiveSentenceCardConfig().audioField || 'SentenceAudio',
);
}
private createPendingNoteInfo(fields: Record<string, string>): CardCreationNoteInfo {
return {
noteId: -1,
@@ -34,6 +34,18 @@ test('guessAnilistMediaInfo fills missing guessit episode from filename parser',
});
});
test('guessAnilistMediaInfo ignores low-confidence parser details when guessit omits them', async () => {
const result = await guessAnilistMediaInfo('/tmp/Season 2/Guessit Title.mkv', null, {
runGuessit: async () => JSON.stringify({ title: 'Guessit Title' }),
});
assert.deepEqual(result, {
title: 'Guessit Title',
season: null,
episode: null,
source: 'guessit',
});
});
test('guessAnilistMediaInfo parses Little Witch Academia release filename', async () => {
const filename =
'/tmp/Little Witch Academia (2017) - S01E02 - 002 - Papiliodia [Bluray-1080p][10bit][h265][AC3 2.0][JA].mkv';
+3 -2
View File
@@ -237,12 +237,13 @@ export async function guessAnilistMediaInfo(
const year = firstYear(parsed.year);
if (title) {
const fallback = parseMediaInfo(target);
const canUseFallbackDetails = fallback.confidence !== 'low';
return {
title: buildGuessitTitle(title, alternativeTitle),
...(alternativeTitle ? { alternativeTitle } : {}),
...(year ? { year } : {}),
season: season ?? fallback.season,
episode: episode ?? fallback.episode,
season: season ?? (canUseFallbackDetails ? fallback.season : null),
episode: episode ?? (canUseFallbackDetails ? fallback.episode : null),
source: 'guessit',
};
}
@@ -567,6 +567,19 @@ test('shouldExcludeTokenFromSubtitleAnnotations excludes standalone connective p
assert.equal(shouldExcludeTokenFromSubtitleAnnotations(token), true);
});
test('shouldExcludeTokenFromSubtitleAnnotations keeps lexical verbs whose reading matches connective particles', () => {
const token = makeToken({
surface: '立って',
headword: '立つ',
reading: 'タッテ',
partOfSpeech: PartOfSpeech.verb,
pos1: '動詞',
pos2: '自立',
});
assert.equal(shouldExcludeTokenFromSubtitleAnnotations(token), false);
});
test('shouldExcludeTokenFromSubtitleAnnotations excludes rhetorical もんか grammar particle phrases', () => {
for (const surface of ['もんか', 'ものか']) {
const token = makeToken({
@@ -57,7 +57,6 @@ export const SUBTITLE_ANNOTATION_EXCLUDED_TERMS = new Set([
'貴方',
'もんか',
'ものか',
...STANDALONE_GRAMMAR_PARTICLE_PHRASES,
]);
const SUBTITLE_ANNOTATION_EXCLUDED_TRAILING_PARTICLE_SUFFIXES = new Set([
'って',
@@ -223,6 +223,41 @@ test('time-pos and pause handlers report progress with correct urgency', () => {
]);
});
test('time-pos handler logs post-watch update rejection without blocking later handlers', async () => {
const calls: string[] = [];
const timeHandler = createHandleMpvTimePosChangeHandler({
recordPlaybackPosition: (time) => calls.push(`time:${time}`),
reportJellyfinRemoteProgress: (force) => calls.push(`progress:${force ? 'force' : 'normal'}`),
refreshDiscordPresence: () => calls.push('presence'),
maybeRunAnilistPostWatchUpdate: async () => {
calls.push('post-watch');
throw new Error('boom');
},
logError: (message, error) => calls.push(`error:${message}:${(error as Error).message}`),
});
const pauseHandler = createHandleMpvPauseChangeHandler({
recordPauseState: (paused) => calls.push(`pause:${paused ? 'yes' : 'no'}`),
reportJellyfinRemoteProgress: (force) => calls.push(`progress:${force ? 'force' : 'normal'}`),
refreshDiscordPresence: () => calls.push('presence'),
});
timeHandler({ time: 12.5 });
pauseHandler({ paused: true });
await Promise.resolve();
await Promise.resolve();
assert.deepEqual(calls, [
'time:12.5',
'progress:normal',
'presence',
'post-watch',
'pause:yes',
'progress:force',
'presence',
'error:AniList post-watch update failed unexpectedly:boom',
]);
});
test('subtitle metrics change handler forwards patch payload', () => {
let received: Record<string, unknown> | null = null;
const handler = createHandleMpvSubtitleMetricsChangeHandler({
+8 -12
View File
@@ -78,23 +78,19 @@ export function createBuildBindMpvMainEventHandlersMainDepsHandler(deps: {
deps.ensureImmersionTrackerInitialized();
deps.appState.immersionTracker?.recordPlaybackPosition?.(normalizedTimeSec);
};
const hasInitialPlaybackQuitOnDisconnectArg = (): boolean =>
Boolean(
deps.appState.initialArgs?.managedPlayback ||
deps.appState.initialArgs?.jellyfinPlay ||
deps.appState.initialArgs?.youtubePlay,
);
return () => ({
reportJellyfinRemoteStopped: () => deps.reportJellyfinRemoteStopped(),
syncOverlayMpvSubtitleSuppression: () => deps.syncOverlayMpvSubtitleSuppression(),
hasInitialPlaybackQuitOnDisconnectArg: () =>
Boolean(
deps.appState.initialArgs?.managedPlayback ||
deps.appState.initialArgs?.jellyfinPlay ||
deps.appState.initialArgs?.youtubePlay,
),
hasInitialPlaybackQuitOnDisconnectArg,
isOverlayRuntimeInitialized: () => deps.appState.overlayRuntimeInitialized,
shouldQuitOnDisconnectWhenOverlayRuntimeInitialized: () =>
Boolean(
deps.appState.initialArgs?.managedPlayback ||
deps.appState.initialArgs?.jellyfinPlay ||
deps.appState.initialArgs?.youtubePlay,
),
shouldQuitOnDisconnectWhenOverlayRuntimeInitialized: hasInitialPlaybackQuitOnDisconnectArg,
isQuitOnDisconnectArmed: () => deps.getQuitOnDisconnectArmed(),
scheduleQuitCheck: (callback: () => void) => deps.scheduleQuitCheck(callback),
isMpvConnected: () => Boolean(deps.appState.mpvClient?.connected),
+1
View File
@@ -909,6 +909,7 @@ test('subtitle annotation CSS underlines JLPT tokens without changing token colo
// popup/selection state.
assert.doesNotMatch(plainJlptBlock, /(?:^|\n)\s*color\s*:/m);
assert.doesNotMatch(plainJlptBlock, /text-decoration-line:\s*underline;/);
assert.doesNotMatch(plainJlptBlock, /text-decoration\s*:[^;]*\bunderline\b/i);
assert.match(
plainJlptBlock,
new RegExp(`border-bottom:\\s*2px\\s+solid\\s+var\\(--subtitle-jlpt-n${level}-color,`),