fix(anki): respect configured fields for word-card enrichment

- Keep dedicated Lapis/Kiku sentence-card fields unchanged
This commit is contained in:
2026-08-28 23:35:59 -07:00
parent 6e945f0872
commit 299c1d3173
11 changed files with 236 additions and 59 deletions
@@ -0,0 +1,4 @@
type: fixed
area: anki
- Word-card enrichment now always writes sentence text and audio to the configured AnkiConnect fields, while dedicated Lapis/Kiku sentence-card and audio-card actions keep their compatible field names.
+5 -1
View File
@@ -136,6 +136,8 @@ SubMiner maps its data to your Anki note fields. Configure these under `ankiConn
Field names are matched against your Anki note type case-insensitively (an exact match wins, then a lowercase comparison). If a configured field does not exist on the note type, SubMiner skips it without error.
These mappings always control normal word-card enrichment, including Yomitan proxy/polling updates and manual clipboard updates. Enabling Lapis or Kiku does not replace the configured word-card sentence and audio fields with `Sentence` and `SentenceAudio`. The dedicated sentence-card and audio-card shortcuts still use those Lapis/Kiku field names.
Two related options live alongside `fields`: `ankiConnect.deck` (target deck; empty falls back as described above) and `ankiConnect.tags` (tags added to mined cards, default `["SubMiner"]`; set `[]` to disable tagging). The `miscInfo` content is controlled by `ankiConnect.metadata.pattern` (default `[SubMiner] %f (%t)`; tokens: `%f` filename, `%F` filename with extension, `%t` timestamp, `%T` timestamp with milliseconds, `<br>` newline).
### Minimal Config
@@ -233,7 +235,7 @@ Animated AVIF requires an AV1 encoder (`libaom-av1`, `libsvtav1`, or `librav1e`)
When media is available, mined-card overlay and system notifications include the same current-frame thumbnail.
`overwriteAudio` applies to automatic card updates and duplicate-card enrichment. Manual clipboard subtitle updates (`Ctrl/Cmd+C`, then `Ctrl/Cmd+V`) always replace generated sentence audio, while leaving the word audio field unchanged.
`overwriteAudio` applies to automatic card updates and duplicate-card enrichment. Manual clipboard subtitle updates (`Ctrl/Cmd+C`, then `Ctrl/Cmd+V`) always replace generated sentence audio in `ankiConnect.fields.audio`, even when `overwriteAudio` is disabled.
## AI Translation
@@ -287,6 +289,8 @@ Sentence card creation and audio card marking require a non-empty `ankiConnect.i
Trigger with the mine sentence shortcut (`Ctrl/Cmd+S` by default). The card is created directly via AnkiConnect with the sentence, audio, and image filled in.
The dedicated sentence-card and audio-card shortcuts use the Lapis/Kiku-compatible `Sentence` and `SentenceAudio` fields. This does not affect the configured fields used to enrich normal word cards.
To mine multiple subtitle lines as one sentence card, use `Ctrl/Cmd+Shift+S` followed by a digit (19) to select how many recent lines to combine.
## Word Card Type (Kiku/Lapis)
+1 -1
View File
@@ -41,7 +41,7 @@ If you prefer a hands-on approach (animecards-style), you can copy the current s
- For multiple lines: press `Ctrl/Cmd+Shift+C`, then a digit `1``9` to select how many recent subtitle lines to combine. The combined text is copied to the clipboard.
3. Press `Ctrl/Cmd+V` to update the last-added card with the clipboard contents plus audio, image, and translation - the same fields auto-update would fill.
Manual clipboard updates always replace generated sentence audio, even when `ankiConnect.behavior.overwriteAudio` is disabled. The word audio field is left unchanged because the word itself does not change in this flow.
Manual clipboard updates always replace generated sentence audio in `ankiConnect.fields.audio`, even when `ankiConnect.behavior.overwriteAudio` is disabled. Normal word-card updates use the configured sentence and audio fields even when Lapis or Kiku support is enabled.
This is useful when auto-update is disabled or when you want explicit control over which subtitle line gets attached to the card.
+5 -3
View File
@@ -608,6 +608,7 @@ test('AnkiIntegration applies ready YouTube cache media to every queued note id'
const integration = new AnkiIntegration(
{
fields: {
audio: 'ExpressionAudio',
image: 'Picture',
},
media: {
@@ -661,7 +662,7 @@ test('AnkiIntegration applies ready YouTube cache media to every queued note id'
noteIds.map((noteId) => ({
noteId,
fields: {
SentenceAudio: { value: '' },
ExpressionAudio: { value: '' },
Picture: { value: '' },
},
})),
@@ -946,7 +947,7 @@ test('AnkiIntegration queues YouTube media updates against recovered source URLs
noteInfo: {
noteId: 404,
fields: {
SentenceAudio: { value: '' },
ExpressionAudio: { value: '' },
Picture: { value: '' },
},
},
@@ -958,7 +959,8 @@ test('AnkiIntegration queues YouTube media updates against recovered source URLs
assert.equal(queued, true);
assert.equal(updatedNotes.length, 1);
assert.equal(updatedNotes[0]?.noteId, 404);
assert.match(updatedNotes[0]?.fields.SentenceAudio ?? '', /^\[sound:audio_/);
assert.match(updatedNotes[0]?.fields.ExpressionAudio ?? '', /^\[sound:audio_/);
assert.equal(updatedNotes[0]?.fields.SentenceAudio, undefined);
assert.match(updatedNotes[0]?.fields.Picture ?? '', /^<img src="image_/);
assert.equal(storedMedia.length, 2);
assert.deepEqual(audioVolumeScales, [0.3 ** 3]);
-11
View File
@@ -383,8 +383,6 @@ export class AnkiIntegration {
getCachedMediaPath: this.getCachedMediaPath,
shouldRequireRemoteMediaCache: () => this.shouldRequireRemoteMediaCache?.() === true,
getSubtitleMediaRange: (context) => this.getSubtitleMediaRange(context),
getResolvedSentenceAudioFieldName: (noteInfo) =>
this.getResolvedSentenceAudioFieldName(noteInfo),
resolveConfiguredFieldName: (noteInfo, ...preferredNames) =>
this.resolveConfiguredFieldName(noteInfo, ...preferredNames),
mergeFieldValue: (existing, newValue, overwrite) =>
@@ -661,8 +659,6 @@ export class AnkiIntegration {
this.setCardTypeFields(updatedFields, availableFieldNames, cardKind),
resolveConfiguredFieldName: (noteInfo, ...preferredNames) =>
this.resolveConfiguredFieldName(noteInfo, ...preferredNames),
getResolvedSentenceAudioFieldName: (noteInfo) =>
this.getResolvedSentenceAudioFieldName(noteInfo),
getAnimatedImageLeadInSeconds: (noteInfo) => this.getAnimatedImageLeadInSeconds(noteInfo),
mergeFieldValue: (existing, newValue, overwrite) =>
this.mergeFieldValue(existing, newValue, overwrite),
@@ -1567,13 +1563,6 @@ export class AnkiIntegration {
return sentenceCardConfig.audioField || 'SentenceAudio';
}
private getResolvedSentenceAudioFieldName(noteInfo: NoteInfo): string | null {
return (
this.resolveNoteFieldName(noteInfo, this.getPreferredSentenceAudioFieldName()) ||
this.resolveConfiguredFieldName(noteInfo, this.config.fields?.audio)
);
}
private getConfiguredWordFieldName(): string {
return getConfiguredWordFieldName(this.config);
}
@@ -143,7 +143,7 @@ function createManualUpdateService(overrides: Partial<CardCreationDeps> = {}): {
};
}
test('manual clipboard subtitle update replaces sentence audio without touching expression audio', async () => {
test('manual clipboard subtitle update replaces audio in the configured field', async () => {
const { service, updatedFields, mergeCalls, storedMedia } = createManualUpdateService();
await service.updateLastAddedFromClipboard('字幕');
@@ -151,14 +151,144 @@ test('manual clipboard subtitle update replaces sentence audio without touching
assert.equal(updatedFields.length, 1);
assert.equal(storedMedia.length, 1);
const audioValue = `[sound:${storedMedia[0]}]`;
assert.equal(updatedFields[0]?.SentenceAudio, audioValue);
assert.equal('ExpressionAudio' in updatedFields[0]!, false);
assert.equal(updatedFields[0]?.ExpressionAudio, audioValue);
assert.equal('SentenceAudio' in updatedFields[0]!, false);
assert.deepEqual(
mergeCalls.map((call) => call.overwrite),
[true],
);
});
test('manual clipboard word-card update uses configured fields with Lapis and Kiku enabled', async () => {
const { service, updatedFields } = createManualUpdateService({
getConfig: () =>
({
deck: 'Mining',
fields: {
word: 'Expression',
sentence: 'Context',
audio: 'ContextAudio',
},
media: {
generateAudio: true,
generateImage: false,
maxMediaDuration: 30,
},
behavior: {
overwriteAudio: false,
overwriteImage: false,
},
ai: false,
}) as AnkiConnectConfig,
client: {
addNote: async () => 0,
addTags: async () => undefined,
notesInfo: async () => [
{
noteId: 42,
fields: {
Expression: { value: '単語' },
Sentence: { value: '' },
SentenceAudio: { value: '' },
Context: { value: '' },
ContextAudio: { value: '' },
},
},
],
updateNoteFields: async (_noteId, fields) => {
updatedFields.push(fields);
},
storeMediaFile: async () => undefined,
findNotes: async () => [42],
retrieveMediaFile: async () => '',
},
getEffectiveSentenceCardConfig: () => ({
model: 'Sentence',
sentenceField: 'Sentence',
audioField: 'SentenceAudio',
lapisEnabled: true,
kikuEnabled: true,
kikuFieldGrouping: 'disabled',
kikuDeleteDuplicateInAuto: false,
}),
});
await service.updateLastAddedFromClipboard('字幕');
assert.equal(updatedFields.length, 1);
assert.match(updatedFields[0]?.ContextAudio ?? '', /^\[sound:audio_\d+\.mp3\]$/);
assert.deepEqual(Object.keys(updatedFields[0] ?? {}).sort(), ['Context', 'ContextAudio']);
assert.equal(updatedFields[0]?.Context, '字幕');
});
test('audio-card action keeps Lapis and Kiku sentence fields', async () => {
const { service, updatedFields } = createManualUpdateService({
getConfig: () =>
({
deck: 'Mining',
fields: {
word: 'Expression',
sentence: 'Context',
audio: 'ContextAudio',
},
media: {
generateAudio: true,
generateImage: false,
maxMediaDuration: 30,
},
behavior: {},
ai: false,
}) as AnkiConnectConfig,
getMpvClient: () =>
({
currentVideoPath: '/video.mp4',
currentAudioStreamIndex: 0,
currentSubText: '字幕',
currentSubStart: 12,
currentSubEnd: 14,
}) as never,
client: {
addNote: async () => 0,
addTags: async () => undefined,
notesInfo: async () => [
{
noteId: 42,
fields: {
Expression: { value: '単語' },
Sentence: { value: '' },
SentenceAudio: { value: '' },
Context: { value: '' },
ContextAudio: { value: '' },
},
},
],
updateNoteFields: async (_noteId, fields) => {
updatedFields.push(fields);
},
storeMediaFile: async () => undefined,
findNotes: async () => [42],
retrieveMediaFile: async () => '',
},
getEffectiveSentenceCardConfig: () => ({
model: 'Sentence',
sentenceField: 'Sentence',
audioField: 'SentenceAudio',
lapisEnabled: true,
kikuEnabled: true,
kikuFieldGrouping: 'disabled',
kikuDeleteDuplicateInAuto: false,
}),
});
await service.markLastCardAsAudioCard();
assert.equal(updatedFields.length, 1);
assert.equal(updatedFields[0]?.Sentence, '字幕');
assert.match(updatedFields[0]?.SentenceAudio ?? '', /^\[sound:audio_\d+\.mp3\]$/);
assert.equal('Context' in (updatedFields[0] ?? {}), false);
assert.equal('ContextAudio' in (updatedFields[0] ?? {}), false);
});
test('manual clipboard subtitle update marks Kiku word cards as word-and-sentence cards when enabled', async () => {
const { service, updatedFields } = createManualUpdateService({
getConfig: () =>
@@ -225,7 +355,7 @@ test('manual clipboard subtitle update marks Kiku word cards as word-and-sentenc
});
});
test('manual clipboard subtitle update skips audio when sentence audio field is missing', async () => {
test('manual clipboard subtitle update uses configured audio when SentenceAudio is missing', async () => {
const { service, updatedFields, mergeCalls, storedMedia } = createManualUpdateService({
client: {
addNote: async () => 0,
@@ -255,8 +385,9 @@ test('manual clipboard subtitle update skips audio when sentence audio field is
assert.equal(storedMedia.length, 1);
assert.equal(updatedFields.length, 1);
assert.deepEqual(updatedFields[0], { Sentence: '字幕' });
assert.equal(mergeCalls.length, 0);
assert.match(updatedFields[0]?.ExpressionAudio ?? '', /^\[sound:audio_\d+\.mp3\]$/);
assert.equal(updatedFields[0]?.Sentence, '字幕');
assert.equal(mergeCalls.length, 1);
});
test('manual clipboard subtitle update uses resolved mpv stream URLs for remote media', async () => {
+11 -21
View File
@@ -260,9 +260,16 @@ export class CardCreationService {
fields,
this.deps.getConfig(),
);
const sentenceAudioField = this.getResolvedSentenceOnlyAudioFieldName(noteInfo);
const config = this.deps.getConfig();
const sentenceAudioField = this.deps.resolveConfiguredFieldName(
noteInfo,
config.fields?.audio ?? DEFAULT_ANKI_CONNECT_CONFIG.fields.audio,
);
const sentenceCardConfig = this.deps.getEffectiveSentenceCardConfig();
const sentenceField = sentenceCardConfig.sentenceField;
const sentenceField = this.deps.resolveConfiguredFieldName(
noteInfo,
config.fields?.sentence ?? DEFAULT_ANKI_CONNECT_CONFIG.fields.sentence,
);
const sentence = blocks.join(' ');
const updatedFields: Record<string, string> = {};
@@ -284,7 +291,6 @@ export class CardCreationService {
`Clipboard update: timing range ${rangeStart.toFixed(2)}s - ${rangeEnd.toFixed(2)}s`,
);
const config = this.deps.getConfig();
const generateAudio = shouldGenerateAudio(config);
const generateImage = shouldGenerateImage(config);
const mediaResolverOptions = this.getMediaResolverOptions();
@@ -457,13 +463,13 @@ export class CardCreationService {
this.deps.setCardTypeFields(updatedFields, Object.keys(noteInfo.fields), 'audio');
const sentenceField = this.deps.getConfig().fields?.sentence;
const sentenceCardConfig = this.deps.getEffectiveSentenceCardConfig();
const sentenceField = sentenceCardConfig.sentenceField;
if (sentenceField) {
const processedSentence = this.deps.processSentence(mpvClient.currentSubText, fields);
updatedFields[sentenceField] = processedSentence;
}
const sentenceCardConfig = this.deps.getEffectiveSentenceCardConfig();
const audioFieldName = sentenceCardConfig.audioField;
try {
const audioFilename = this.generateAudioFilename();
@@ -806,22 +812,6 @@ export class CardCreationService {
}
}
private getResolvedSentenceAudioFieldName(noteInfo: CardCreationNoteInfo): string | null {
return (
this.deps.resolveNoteFieldName(
noteInfo,
this.deps.getEffectiveSentenceCardConfig().audioField || 'SentenceAudio',
) || this.deps.resolveConfiguredFieldName(noteInfo, this.deps.getConfig().fields?.audio)
);
}
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,
@@ -80,7 +80,6 @@ function createWorkflowHarness() {
const names = Object.keys(noteInfo.fields);
return names.find((name) => name.toLowerCase() === preferred.toLowerCase()) ?? null;
},
getResolvedSentenceAudioFieldName: () => null,
getAnimatedImageLeadInSeconds: async () => 0,
mergeFieldValue: (_existing: string, next: string, _overwrite: boolean) => next,
generateAudioFilename: () => 'audio_1.mp3',
@@ -120,6 +119,49 @@ test('NoteUpdateWorkflow updates sentence field and emits notification', async (
assert.equal(harness.notifications.length, 1);
});
test('NoteUpdateWorkflow uses configured fields for word-card enrichment with Lapis and Kiku enabled', async () => {
const harness = createWorkflowHarness();
harness.deps.getConfig = () => ({
fields: {
sentence: 'Context',
audio: 'ContextAudio',
},
media: {
generateAudio: true,
generateImage: false,
},
behavior: {},
});
harness.deps.getEffectiveSentenceCardConfig = () => ({
sentenceField: 'Sentence',
lapisEnabled: true,
kikuEnabled: true,
kikuFieldGrouping: 'disabled',
});
harness.deps.client.notesInfo = async () =>
[
{
noteId: 42,
fields: {
Expression: { value: 'taberu' },
Sentence: { value: '' },
SentenceAudio: { value: '' },
Context: { value: '' },
ContextAudio: { value: '' },
},
},
] satisfies NoteUpdateWorkflowNoteInfo[];
harness.deps.generateAudio = async () => Buffer.from('audio');
await harness.workflow.execute(42);
assert.equal(harness.updates.length, 1);
assert.deepEqual(harness.updates[0]?.fields, {
Context: 'subtitle-text',
ContextAudio: '[sound:audio_1.mp3]',
});
});
test('NoteUpdateWorkflow updates sentence furigana when highlight processor changes it', async () => {
const harness = createWorkflowHarness();
harness.deps.client.notesInfo = async () =>
@@ -432,6 +474,7 @@ test('NoteUpdateWorkflow uses subtitle sidebar context for sentence media timing
harness.deps.getConfig = () => ({
fields: {
sentence: 'Sentence',
audio: 'SentenceAudio',
image: 'Picture',
miscInfo: 'MiscInfo',
},
@@ -444,7 +487,6 @@ test('NoteUpdateWorkflow uses subtitle sidebar context for sentence media timing
});
harness.deps.getCurrentSubtitleText = () => 'current primary line';
harness.deps.getCurrentSubtitleStart = () => 20;
harness.deps.getResolvedSentenceAudioFieldName = () => 'SentenceAudio';
harness.deps.generateAudio = async (context?: SubtitleMiningContext) => {
audioContext = context ?? null;
return Buffer.from('audio');
@@ -501,6 +543,7 @@ test('NoteUpdateWorkflow snapshots one media range for audio and image without a
harness.deps.getConfig = () => ({
fields: {
sentence: 'Sentence',
audio: 'SentenceAudio',
image: 'Picture',
miscInfo: 'MiscInfo',
},
@@ -511,7 +554,6 @@ test('NoteUpdateWorkflow snapshots one media range for audio and image without a
},
behavior: {},
});
harness.deps.getResolvedSentenceAudioFieldName = () => 'SentenceAudio';
harness.deps.captureSubtitleMediaContext = () => {
captureCalls += 1;
return capturedContext;
+10 -5
View File
@@ -19,6 +19,7 @@ export interface NoteUpdateWorkflowDeps {
fields?: {
word?: string;
sentence?: string;
audio?: string;
image?: string;
miscInfo?: string;
};
@@ -75,7 +76,6 @@ export interface NoteUpdateWorkflowDeps {
noteInfo: NoteUpdateWorkflowNoteInfo,
...preferredNames: (string | undefined)[]
) => string | null;
getResolvedSentenceAudioFieldName: (noteInfo: NoteUpdateWorkflowNoteInfo) => string | null;
getAnimatedImageLeadInSeconds: (noteInfo: NoteUpdateWorkflowNoteInfo) => Promise<number>;
mergeFieldValue: (existing: string, newValue: string, overwrite: boolean) => string;
generateAudioFilename: () => string;
@@ -198,11 +198,13 @@ export class NoteUpdateWorkflow {
const updatedFields: Record<string, string> = {};
let updatePerformed = false;
let miscInfoFilename: string | null = null;
const sentenceField = sentenceCardConfig.sentenceField;
const configuredSentenceField =
config.fields?.sentence ?? DEFAULT_ANKI_CONNECT_CONFIG.fields.sentence;
const sentenceField = this.deps.resolveConfiguredFieldName(noteInfo, configuredSentenceField);
const subtitleMiningContext = this.consumeMatchingSubtitleMiningContext(
fields,
sentenceField,
config.fields?.sentence,
sentenceField ?? configuredSentenceField,
configuredSentenceField,
);
// Audio and image generation run sequentially and audio extraction can take tens of
// seconds, so resolve the clip range exactly once up front; reading live mpv sub
@@ -258,7 +260,10 @@ export class NoteUpdateWorkflow {
if (audioBuffer) {
await this.deps.client.storeMediaFile(audioFilename, audioBuffer);
const sentenceAudioField = this.deps.getResolvedSentenceAudioFieldName(noteInfo);
const sentenceAudioField = this.deps.resolveConfiguredFieldName(
noteInfo,
config.fields?.audio ?? DEFAULT_ANKI_CONNECT_CONFIG.fields.audio,
);
if (sentenceAudioField) {
const existingAudio = noteInfo.fields[sentenceAudioField]?.value || '';
updatedFields[sentenceAudioField] = this.deps.mergeFieldValue(
@@ -31,7 +31,6 @@ function createDeps(
getCachedMediaPath: async () => null,
shouldRequireRemoteMediaCache: () => true,
getSubtitleMediaRange: () => ({ startTime: 1, endTime: 2 }),
getResolvedSentenceAudioFieldName: () => 'SentenceAudio',
resolveConfiguredFieldName: () => 'Picture',
mergeFieldValue: (_existing, newValue) => newValue,
getAnimatedImageLeadInSeconds: async () => 0,
@@ -133,7 +132,7 @@ test('PendingYoutubeMediaQueue defaults missing media flags to enabled when queu
noteIds.map((noteId) => ({
noteId,
fields: {
SentenceAudio: { value: '' },
ExpressionAudio: { value: '' },
Picture: { value: '' },
},
})),
@@ -144,13 +143,16 @@ test('PendingYoutubeMediaQueue defaults missing media flags to enabled when queu
storedMedia.push(filename);
},
},
getConfig: () => ({ media: {}, fields: { image: 'Picture' } }) as AnkiConnectConfig,
getConfig: () =>
({ media: {}, fields: { audio: 'ExpressionAudio', image: 'Picture' } }) as AnkiConnectConfig,
resolveConfiguredFieldName: (noteInfo, ...preferredNames) =>
preferredNames.find((name) => name && name in noteInfo.fields) ?? null,
});
const queue = new PendingYoutubeMediaQueue(deps);
const queued = await queue.queueFromNote({
noteId: 42,
noteInfo: { noteId: 42, fields: {} },
noteInfo: { noteId: 42, fields: { ExpressionAudio: { value: '' } } },
label: 'demo',
});
await queue.handleReady('https://youtu.be/abc123', '/tmp/media.mkv');
@@ -158,7 +160,8 @@ test('PendingYoutubeMediaQueue defaults missing media flags to enabled when queu
assert.equal(queued, true);
assert.equal(updatedNotes.length, 1);
assert.equal(storedMedia.length, 2);
assert.match(updatedNotes[0]?.fields.SentenceAudio ?? '', /^\[sound:audio\.mp3\]$/);
assert.match(updatedNotes[0]?.fields.ExpressionAudio ?? '', /^\[sound:audio\.mp3\]$/);
assert.equal('SentenceAudio' in (updatedNotes[0]?.fields ?? {}), false);
assert.match(updatedNotes[0]?.fields.Picture ?? '', /^<img src="image\.webp">$/);
});
@@ -39,7 +39,6 @@ export interface PendingYoutubeMediaQueueDeps {
startTime: number;
endTime: number;
};
getResolvedSentenceAudioFieldName: (noteInfo: PendingYoutubeMediaNoteInfo) => string | null;
resolveConfiguredFieldName: (
noteInfo: PendingYoutubeMediaNoteInfo,
...preferredNames: (string | undefined)[]
@@ -136,7 +135,7 @@ export class PendingYoutubeMediaQueue {
startTime: mediaRange.startTime,
endTime: mediaRange.endTime,
label: job.label,
audioFieldName: this.deps.getResolvedSentenceAudioFieldName(job.noteInfo) ?? undefined,
audioFieldName: this.resolveConfiguredAudioFieldName(job.noteInfo) ?? undefined,
imageFieldName:
this.deps.resolveConfiguredFieldName(
job.noteInfo,
@@ -247,6 +246,14 @@ export class PendingYoutubeMediaQueue {
return matched;
}
private resolveConfiguredAudioFieldName(noteInfo: PendingYoutubeMediaNoteInfo): string | null {
const config = this.deps.getConfig();
return this.deps.resolveConfiguredFieldName(
noteInfo,
config.fields?.audio ?? DEFAULT_ANKI_CONNECT_CONFIG.fields.audio,
);
}
private async applyUpdate(
job: PendingYoutubeMediaUpdate,
cachedPath: string,
@@ -283,7 +290,7 @@ export class PendingYoutubeMediaQueue {
if (audioBuffer) {
await this.deps.client.storeMediaFile(audioFilename, audioBuffer);
const audioField =
job.audioFieldName || this.deps.getResolvedSentenceAudioFieldName(noteInfo) || null;
job.audioFieldName || this.resolveConfiguredAudioFieldName(noteInfo) || null;
if (audioField) {
const existingAudio = noteInfo.fields[audioField]?.value || '';
mediaFields[audioField] = this.deps.mergeFieldValue(