diff --git a/scripts/populate-from-wanikani.ts b/scripts/populate-from-wanikani.ts index ce2dcbe..def664a 100644 --- a/scripts/populate-from-wanikani.ts +++ b/scripts/populate-from-wanikani.ts @@ -1,89 +1,9 @@ -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, - }, - })), - }); - } - }); -} +import { WaniKani } from '@/wanikani'; if (require.main === module) { - new WaniKani().subjects().then((subjects) => { - populateSounds('note:jp.takoboto', 'Japanese', 'JapaneseAudio', subjects); - }); + new WaniKani().populateSound( + 'note:jp.takoboto', + { ja: 'Japanese', audio: 'JapaneseAudio' }, + { mode: { online: true } }, + ); } diff --git a/scripts/query-anki.ts b/scripts/query-anki.ts index 9a8a848..0f5d503 100644 --- a/scripts/query-anki.ts +++ b/scripts/query-anki.ts @@ -40,7 +40,7 @@ export async function addSoundTag(query: string, fieldNames: string[]) { const { value } = field; if (!soundTag.is(value)) { toUpdate = toUpdate || { id: n.noteId, fields: {} }; - toUpdate.fields[fieldName] = soundTag.add(value); + toUpdate.fields[fieldName] = soundTag.make(value); } } }); diff --git a/src/anki.ts b/src/anki.ts index e420857..720ae9a 100644 --- a/src/anki.ts +++ b/src/anki.ts @@ -53,16 +53,28 @@ export const soundTag = { 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; + make(src: string) { + if (this.is(src)) return src; + const out = this.pre + src + this.post; + if (this.isBroken(out)) return src; + return out; }, - remove(src: string) { - if (!soundTag.is(src)) return src; + clean(src: string) { + if (this.isBroken(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; }, + isBroken(src: string) { + if (this.is(src)) { + return ( + src.indexOf(this.pre) > 0 || + src.indexOf(this.post) < src.length - this.post.length + ); + } + + return false; + }, }; diff --git a/src/ankiconnect.ts b/src/ankiconnect.ts index 33808b3..95492b5 100644 --- a/src/ankiconnect.ts +++ b/src/ankiconnect.ts @@ -266,7 +266,7 @@ export interface IAnkiConnectActions params: { note: { id: number; - fields: { + fields?: { [fieldName: string]: string; }; audio?: IAnkiConnectMediaInNote[]; diff --git a/src/wanikani.ts b/src/wanikani.ts index 5286f3d..6117c59 100644 --- a/src/wanikani.ts +++ b/src/wanikani.ts @@ -2,14 +2,18 @@ import { existsSync, readFileSync, writeFileSync } from 'fs'; import axios, { AxiosInstance } from 'axios'; +import { soundTag } from './anki'; +import { AnkiConnect, IAnkiConnectActions } from './ankiconnect'; +import { logger } from './logger'; + export class WaniKani { $axios: AxiosInstance; - constructor(public apiKey = process.env['WANIKANI_API_KEY']!) { + constructor(public apiKey = process.env['WANIKANI_API_KEY']) { this.$axios = axios.create({ baseURL: 'https://api.wanikani.com/v2/', headers: { - Authorization: `Bearer ${process.env['WANIKANI_API_KEY']}`, + Authorization: apiKey ? `Bearer ${apiKey}` : '', }, }); } @@ -54,13 +58,108 @@ export class WaniKani { }>(nextURL) .then((r) => r.data); data.push(...r.data); - console.info(r.pages.next_url); + logger('WaniKani API:', r.pages.next_url); nextURL = r.pages.next_url || ''; } subjects.dump(data); return data; } + + async populateSound( + query: string, + fields: { + ja: string; + audio: string; + }, + opts: { + mode?: { + online?: boolean; + }; + } = {}, + ) { + const subjects = await this.subjects(); + + 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 += ` -${fields.ja}: ${fields.audio}:`; + + 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[fields.ja] || {}; + if (ja) { + const audio = audioMap.get( + ja.replace(/\[.+?\]/g, '').replace(/ /g, ''), + ); + if (audio) { + notesToUpdate.push( + opts.mode?.online + ? { + id: n.noteId, + fields: { + [fields.audio]: soundTag.make(audio.url), + }, + } + : { + id: n.noteId, + fields: {}, + audio: [ + { + url: audio.url, + filename: audio.filename, + fields: [fields.audio], + }, + ], + }, + ); + } + } + } + + if (!notesToUpdate.length) return; + + while (notesToUpdate.length) { + await anki.multi<'updateNoteFields'[]>({ + actions: notesToUpdate.splice(0, 100).map((note) => ({ + action: 'updateNoteFields', + params: { + note, + }, + })), + }); + } + }); + } } type WaniKaniDate = string;