Extra contents beyond WaniKani
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

148 lines
3.9 KiB

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<number>;
} = {};
const cardsByTemplateName = {
data: new Map<string, number[]>(),
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();
}