mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-22 17:16:19 -07:00
- Add backend selection, setup gating, Anki integration, and external host support - Add launcher flags, documentation, packaging, and focused tests - Open on-demand overlay modals on the first attempt
55 lines
3.3 KiB
JavaScript
55 lines
3.3 KiB
JavaScript
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
import { ankiTemplateMarkerNames } from "./anki-templates.js";
|
|
import { canonicalAnkiFields, overwriteAnkiFields } from "./anki-duplicates.js";
|
|
import { readAnkiNoteFields, verifyAnkiFields } from "./anki-mining.js";
|
|
|
|
function pronunciationFields(incoming, current, appliedFields, existingFields, warnings) {
|
|
const fields = {};
|
|
for (const [field, value] of Object.entries(incoming)) {
|
|
const baseline = appliedFields[field] ?? existingFields?.[field] ?? "";
|
|
if (typeof current[field] !== "string" || current[field].normalize("NFC") !== baseline.normalize("NFC")) {
|
|
warnings.push(`Field “${field}” changed in Anki; its pronunciation update was skipped.`);
|
|
} else if (current[field] !== value) fields[field] = value;
|
|
}
|
|
return fields;
|
|
}
|
|
|
|
export async function enrichAnkiNote(context, { audio, render, store }) {
|
|
const { request, invoke, noteId, appliedFields, existingFields, resolved, resources } = context;
|
|
if (resources.pronunciationWarning) return [resources.pronunciationWarning];
|
|
const warnings = [];
|
|
const confirmed = new Set(Array.isArray(resources.confirmedMedia) ? resources.confirmedMedia : []);
|
|
async function ensure(file, kind) {
|
|
if (confirmed.has(file.filename)) return;
|
|
await store(file, kind);
|
|
confirmed.add(file.filename);
|
|
}
|
|
// Dictionary media and any first-field pronunciation have already been
|
|
// confirmed before the note mutation. Only deferred pronunciation remains.
|
|
const canonical = existingFields ? canonicalAnkiFields({}, resolved.templates, existingFields).templates : resolved.templates;
|
|
const templates = Object.fromEntries(Object.entries(canonical).filter(([field, template]) => {
|
|
if (!ankiTemplateMarkerNames(template.value).includes("audio")) return false;
|
|
return !existingFields || (template.overwriteMode !== "skip"
|
|
&& !(template.overwriteMode === "coalesce" && existingFields[field]));
|
|
}));
|
|
// No enabled audio source means no pronunciation, like a screenshot turned off.
|
|
if (!Object.keys(templates).length || (!resources.audioPrepared && !context.config.audioSources.length)) return warnings;
|
|
try {
|
|
const file = resources.audioPrepared ? resources.audio : await audio(request, context.config);
|
|
await ensure(file, "pronunciation");
|
|
const rendered = await render(request, templates, `[sound:${file.filename}]`, resources);
|
|
const incoming = existingFields ? overwriteAnkiFields(rendered.fields, existingFields, templates, { includeAudio: true }) : rendered.fields;
|
|
// Audio work may have taken seconds. Re-read just before updating; never
|
|
// clobber an external edit or append to our already-applied text a second
|
|
// time. AnkiConnect has no CAS, so the final inter-call race remains.
|
|
const current = await readAnkiNoteFields(invoke, noteId);
|
|
const fields = pronunciationFields(incoming, current, appliedFields, existingFields, warnings);
|
|
if (Object.keys(fields).length) {
|
|
const result = await invoke("updateNoteFields", { note: { id: noteId, fields } }, 10_000);
|
|
if (result !== null) throw new Error("Anki returned an invalid pronunciation-update acknowledgement. Inspect the saved note.");
|
|
await verifyAnkiFields(invoke, noteId, fields);
|
|
}
|
|
} catch (error) { warnings.push(`Pronunciation: ${error.message}`); }
|
|
return warnings;
|
|
}
|