diff --git a/scripts/sort-anki.ts b/scripts/sort-anki.ts index a26a0fc..498d466 100644 --- a/scripts/sort-anki.ts +++ b/scripts/sort-anki.ts @@ -1,4 +1,4 @@ -import { AnkiConnect } from '@/ankiconnect'; +import { AnkiConnect, IAnkiConnectActions } from '@/ankiconnect'; import { makeKanjiLevels } from './get-kanji-level'; @@ -8,95 +8,132 @@ const KANJI_FIELD = 'Japanese'; async function main() { const anki = new AnkiConnect(); - console.log( - await anki - .api('findCards', { - query: `deck:${DECK}`, - }) - .then((cards) => anki.api('cardsInfo', { cards })) - .then(async (rs) => { - const kanjiLevels = await makeKanjiLevels(); - - const modelToSubdecks = new Map< - string, - { - [subdeck: string]: number[]; - } - >(); - rs.map((r) => { - const vs = modelToSubdecks.get(r.modelName) || {}; - const { value } = r.fields[KANJI_FIELD] || {}; - if (value) { - // Get the subdeck names from Kanji - let subdeck = ''; - let level = 0; - - Array.from(value).map((k) => { - const m = kanjiLevels.get(k); - if (m && m.level > level) { - level = m.level; - subdeck = m.deckName; - } - }); + const deckConfig = await anki.api('getDeckConfig', { + deck: DECK, + }); + + await anki + .api('findCards', { + query: `deck:${DECK}`, + }) + .then((cards) => anki.api('cardsInfo', { cards })) + .then(async (rs) => { + const kanjiLevels = await makeKanjiLevels(); - if (subdeck) { - const cardIds = vs[subdeck] || []; - cardIds.push(r.cardId); - vs[subdeck] = cardIds; + const modelToSubdecks = new Map< + string, + { + [subdeck: string]: number[]; + } + >(); + rs.map((r) => { + const vs = modelToSubdecks.get(r.modelName) || {}; + const { value } = r.fields[KANJI_FIELD] || {}; + if (value) { + // Get the subdeck names from Kanji + let subdeck = ''; + let level = 0; - modelToSubdecks.set(r.modelName, vs); + Array.from(value).map((k) => { + const m = kanjiLevels.get(k); + if (m && m.level > level) { + level = m.level; + subdeck = m.deckName; } + }); + + if (subdeck) { + const cardIds = vs[subdeck] || []; + cardIds.push(r.cardId); + vs[subdeck] = cardIds; + + modelToSubdecks.set(r.modelName, vs); } - }); + } + }); - return Promise.all( - Array.from(modelToSubdecks).map(([modelName, subdecks]) => - anki.api('modelTemplates', { modelName }).then((templates) => - Object.entries(subdecks).map(([subdeck, cardIds]) => ({ - modelName, - templateNames: Object.keys(templates), - subdeck, - cardIdsSet: new Set(cardIds), - })), - ), + return Promise.all( + Array.from(modelToSubdecks).map(([modelName, subdecks]) => + anki.api('modelTemplates', { modelName }).then((templates) => + Object.entries(subdecks).map(([subdeck, cardIds]) => ({ + modelName, + templateNames: Object.keys(templates), + subdeck, + cardIdsSet: new Set(cardIds), + })), ), - ); - }) - .then((rs) => { - const cardsByTemplateName = { - data: new Map(), - async get(templateName: string) { - let v = this.data.get(templateName); - if (!v) { - v = await anki.api('findCards', { - query: `deck:${DECK} card:${templateName}`, - }); + ), + ); + }) + .then(async (rs) => { + const remaining: { + [templateName: string]: Set; + } = {}; + + const cardsByTemplateName = { + data: new Map(), + async get(templateName: string) { + let v = this.data.get(templateName); + if (!v) { + v = await anki.api('findCards', { + query: `deck:${DECK} card:${templateName}`, + }); + remaining[templateName] = new Set(v); + } + this.data.set(templateName, v); + return v; + }, + }; + + const toChangeDeck: IAnkiConnectActions['changeDeck']['params'][] = []; + + for (const { templateName, subdeck, cardIdsSet } of rs + .flat() + .flatMap((r) => + r.templateNames.map((templateName) => ({ + ...r, + templateName, + })), + )) { + const cards = await cardsByTemplateName.get(templateName); + + toChangeDeck.push({ + cards: cards.filter((c) => { + const r = remaining[templateName]; + if (r) { + r.delete(c); } - this.data.set(templateName, v); - return v; - }, - }; - - return Promise.all( - rs - .flat() - .flatMap((r) => - r.templateNames.map((templateName) => ({ - ...r, - templateName, - })), - ) - .map(({ templateName, subdeck, cardIdsSet }) => - cardsByTemplateName.get(templateName).then((cards) => - anki.api('changeDeck', { - cards: cards.filter((c) => cardIdsSet.has(c)), - deck: `${DECK}::${subdeck}::${templateName}`, - }), - ), - ), - ); - }), - ); + return cardIdsSet.has(c); + }), + deck: `${DECK}::${subdeck}::${templateName}`, + }); + } + + Object.entries(remaining).map(([templateName, cards]) => { + if (cards.size) { + toChangeDeck.push({ + cards: [...cards], + deck: `${DECK}::独習::${templateName}`, + }); + } + }); + + if (toChangeDeck.length) { + const batchSize = 20; + for (let i = 0; i < toChangeDeck.length; i += batchSize) { + await Promise.all( + toChangeDeck + .slice(i, i + batchSize) + .map((params) => anki.api('changeDeck', params)), + ); + } + + await anki.api('setDeckConfigId', { + decks: toChangeDeck.map((r) => r.deck), + configId: deckConfig.id, + }); + } + }); } if (require.main === module) { diff --git a/src/anki.ts b/src/anki.ts index 04d0e2c..8c20c09 100644 --- a/src/anki.ts +++ b/src/anki.ts @@ -42,3 +42,7 @@ export interface IAnkiTemplate { bqfmt: string; bamt: string; } + +export interface IAnkiDeckConfig { + id: number; +} diff --git a/src/ankiconnect.ts b/src/ankiconnect.ts index d2891cb..33808b3 100644 --- a/src/ankiconnect.ts +++ b/src/ankiconnect.ts @@ -1,6 +1,7 @@ import axios, { AxiosInstance } from 'axios'; -import { IAnkiCard, IAnkiQuery } from './anki'; +import { IAnkiCard, IAnkiDeckConfig, IAnkiQuery } from './anki'; +import { logger } from './logger'; export interface IAnkiConnectActions extends Record< @@ -67,6 +68,19 @@ export interface IAnkiConnectActions }; result: null; }; + getDeckConfig: { + params: { + deck: string; + }; + result: IAnkiDeckConfig; + }; + setDeckConfigId: { + params: { + decks: string[]; + configId: number; + }; + result: boolean; + }; // Media Actions storeMediaFile: { @@ -443,6 +457,8 @@ export class AnkiConnect { params: IAnkiConnectActions[A]['params'], version = this.version, ): Promise { + logger(`AnkiConnect: calling ${action}`, params); + const { data: response } = await this.$axios.post<{ result: any; error: string | null; @@ -461,6 +477,8 @@ export class AnkiConnect { throw response.error; } + logger(`AnkiConnect: finished ${action}`, params); + return response.result; } diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..cfbdd3f --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,5 @@ +export function logger(...args: any[]) { + if (process.env['DEBUG']) { + console.debug(...args); + } +}