diff --git a/scripts/populate-from-wanikani.ts b/scripts/populate-from-wanikani.ts new file mode 100644 index 0000000..ce2dcbe --- /dev/null +++ b/scripts/populate-from-wanikani.ts @@ -0,0 +1,89 @@ +import { soundTag } from '@/anki'; +import { AnkiConnect, IAnkiConnectActions } from '@/ankiconnect'; +import { ISubject, IVocabulary, WaniKani } from '@/wanikani'; + +export async function populateSounds( + query: string, + jaField: string, + audioField: string, + subjects: ISubject[], +) { + const vocabularies = subjects.filter( + (s) => s.object === 'vocabulary', + ) as IVocabulary[]; + const audioMap = new Map< + string, + { + url: string; + filename: string; + } + >(); + vocabularies.map((v) => { + if (audioMap.has(v.data.characters)) return; + const audio = v.data.pronunciation_audios[0]; + if (!audio) return; + audioMap.set(v.data.characters, { + url: audio.url, + filename: `wanikani_${v.data.characters}_${audio.metadata.source_id}${ + audio.content_type === 'audio/ogg' ? '.ogg' : '.mp3' + }`, + }); + }); + if (!audioMap.size) return; + + query += ` -${jaField}: ${audioField}:`; + + const anki = new AnkiConnect(); + anki + .api('findNotes', { + query, + }) + .then((notes) => anki.api('notesInfo', { notes })) + .then(async (notes) => { + const notesToUpdate: IAnkiConnectActions['updateNoteFields']['params']['note'][] = + []; + + for (const n of notes) { + const { value: ja } = n.fields[jaField] || {}; + if (ja) { + const audio = audioMap.get( + ja.replace(/\[.+?\]/g, '').replace(/ /g, ''), + ); + if (audio) { + notesToUpdate.push({ + id: n.noteId, + fields: { + [audioField]: soundTag.add(audio.filename), + }, + audio: [ + { + url: audio.url, + filename: audio.filename, + fields: [audioField], + }, + ], + }); + } + } + } + + if (!notesToUpdate.length) return; + + while (notesToUpdate.length) { + await anki.multi<'updateNoteFields'[]>({ + actions: notesToUpdate.splice(0, 100).map((note) => ({ + action: 'updateNoteFields', + params: { + note, + }, + })), + }); + } + }); +} + +if (require.main === module) { + new WaniKani().subjects().then((subjects) => { + populateSounds('note:jp.takoboto', 'Japanese', 'JapaneseAudio', subjects); + }); +} diff --git a/scripts/query-anki.ts b/scripts/query-anki.ts index 856948a..9a8a848 100644 --- a/scripts/query-anki.ts +++ b/scripts/query-anki.ts @@ -1,3 +1,4 @@ +import { soundTag } from '@/anki'; import { AnkiConnect } from '@/ankiconnect'; export async function listTags( @@ -14,26 +15,6 @@ export async function listTags( }); } -const soundTag = { - pre: '[sound:', - post: ']', - is(src: string) { - return src.startsWith(this.pre) && src.endsWith(this.post); - }, - add(src: string) { - if (soundTag.is(src)) return src; - return this.pre + src + this.post; - }, - remove(src: string) { - if (!soundTag.is(src)) return src; - if (this.isEmpty(src)) return src; - return src.substring(this.pre.length, src.length - this.post.length); - }, - isEmpty(src: string) { - return src === this.pre + this.post; - }, -}; - export async function addSoundTag(query: string, fieldNames: string[]) { query += ' (' + fieldNames.map((f) => `(-${f}: -${f}:[sound:*)`).join(' OR ') + ')'; diff --git a/src/anki.ts b/src/anki.ts index 8c20c09..e420857 100644 --- a/src/anki.ts +++ b/src/anki.ts @@ -46,3 +46,23 @@ export interface IAnkiTemplate { export interface IAnkiDeckConfig { id: number; } + +export const soundTag = { + pre: '[sound:', + post: ']', + is(src: string) { + return src.startsWith(this.pre) && src.endsWith(this.post); + }, + add(src: string) { + if (soundTag.is(src)) return src; + return this.pre + src + this.post; + }, + remove(src: string) { + if (!soundTag.is(src)) return src; + if (this.isEmpty(src)) return src; + return src.substring(this.pre.length, src.length - this.post.length); + }, + isEmpty(src: string) { + return src === this.pre + this.post; + }, +}; diff --git a/src/wanikani.ts b/src/wanikani.ts index 2829b90..5286f3d 100644 --- a/src/wanikani.ts +++ b/src/wanikani.ts @@ -99,14 +99,25 @@ interface ISubjectBase< export type IRadical = ISubjectBase< 'radical', { - character_images: { + character_images: ({ url: string; - metadata: { - inline_styles?: boolean; - dimensions?: string; - }; - content_type: string; - }[]; + } & ( + | { + metadata: { + inline_styles?: boolean; + dimensions?: string; + }; + content_type: 'image/svg+xml'; + } + | { + metadata: { + color: string; + dimensions: string; + style_name: string; + }; + content_type: 'image/png'; + } + ))[]; amalgamation_subject_ids: SubjectID[]; } >; @@ -117,7 +128,8 @@ export type IKanji = ISubjectBase< readings: { reading: string; primary: boolean; - type: 'kunyomi' | 'onyomi'; + accepted_answer: boolean; + type: 'kunyomi' | 'onyomi' | 'nanori'; }[]; component_subject_ids: SubjectID[]; visually_similar_subject_ids: SubjectID[];