import { soundTag } from '@/anki'; import { AnkiConnect, IAnkiConnectActions } from '@/ankiconnect'; export async function listTags( query = 'deck:Takoboto -Takoboto: tag:common -tag:reading-challenge -tag:favorites -tag:death-note -tag:ハピネス -tag:https://www.bunka.go.jp/seisaku/bunkashingikai/kokugo/hokoku/pdf/ijidokun_140221.pdf', ) { const anki = new AnkiConnect(); return anki .api('findNotes', { query, }) .then((notes) => anki.api('notesInfo', { notes })) .then((notes) => { return new Set(notes.flatMap((n) => n.tags)); }); } export async function addSoundTag(query: string, fieldNames: string[]) { query += ' (' + fieldNames.map((f) => `(-${f}: -${f}:[sound:*)`).join(' OR ') + ')'; const anki = new AnkiConnect(); anki .api('findNotes', { query, }) .then((notes) => anki.api('notesInfo', { notes })) .then((notes) => { const notesToUpdate: { id: number; fields: Record; }[] = []; for (const n of notes) { let toUpdate: typeof notesToUpdate[0] | undefined; fieldNames.map((fieldName: string) => { const field = n.fields[fieldName]; if (field) { const { value } = field; if (!soundTag.is(value)) { toUpdate = toUpdate || { id: n.noteId, fields: {} }; toUpdate.fields[fieldName] = soundTag.make(value); } } }); if (toUpdate) { notesToUpdate.push(toUpdate); } } if (!notesToUpdate.length) return; return anki.multi<'updateNoteFields'[]>({ actions: notesToUpdate.map((note) => ({ action: 'updateNoteFields', params: { note, }, })), }); }); } export async function fixCloze( query: string, fields: { vocabJa: string; sentenceCloze: string }, ) { const anki = new AnkiConnect(); anki .api('findNotes', { query, }) .then((notes) => anki.api('notesInfo', { notes })) .then(async (notes) => { const notesToUpdate: IAnkiConnectActions['updateNoteFields']['params']['note'][] = []; const badJa: string[] = []; for (const n of notes) { const { value: ja } = n.fields[fields.vocabJa] || {}; const { value: sent } = n.fields[fields.sentenceCloze] || {}; if (ja && sent) { const clozeChar = '__'; if ( sent .split('
') .filter((_, i) => i % 2 === 0) .every((s) => s.includes(clozeChar)) ) { continue; } const cleanJa = ja.replace(/\[.+?\]/g, '').replace(/ /g, ''); let newSent = sent.replace(cleanJa, clozeChar); const jaNoOkuri = cleanJa.replace(/[\p{sc=Hiragana}]/gu, ''); if (cleanJa !== jaNoOkuri && newSent.includes(jaNoOkuri)) { const s2 = newSent.replace(jaNoOkuri, clozeChar); if (s2 !== newSent) { newSent = s2; } else { badJa.push(ja); } } if (!sent.includes(clozeChar) && sent !== newSent) { notesToUpdate.push({ id: n.noteId, fields: { [fields.sentenceCloze]: newSent, }, }); } } } while (badJa.length) { console.log( query + ' tag:wanikani card:EJ (' + badJa .splice(0, 20) .map((ja) => `${fields.vocabJa}:${ja}`) .join(' OR ') + ')', ); } if (!notesToUpdate.length) return; await anki.multi<'updateNoteFields'[]>({ actions: notesToUpdate.map((note) => ({ action: 'updateNoteFields', params: { note, }, })), }); }); } if (require.main === module) { // addSoundTag('note:jp.takoboto', ['SentenceAudio', 'JapaneseAudio']); fixCloze('deck:Takoboto', { vocabJa: 'Japanese', sentenceCloze: 'MeaningQuiz', }); }