import { AnkiConnect, IAnkiConnectActions } from '@/ankiconnect'; import { makeKanjiLevels } from './get-kanji-level'; const DECK = 'Takoboto'; const KANJI_FIELD = 'Japanese'; /** * Whether to use Level > 60, i.e. outside WaniKani * * @link https://community.wanikani.com/t/fake-levels-61-70-or-%E7%84%A1%E9%99%90-infinity/16399 */ const USE_BEYOND = true; async function main() { const anki = new AnkiConnect(); const deckConfig = await anki.api('getDeckConfig', { deck: DECK, }); const deckQuery = `deck:${DECK} -deck:${DECK}::*`; const kanjiLevels = await makeKanjiLevels({ useBeyond: USE_BEYOND }); await anki .api('findCards', { query: deckQuery, }) .then((cards) => anki.api('cardsInfo', { cards })) .then(async (rs) => { 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; } }); 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), })), ), ), ); }) .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: `${deckQuery} 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); } 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) { main(); }