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();
|
|
}
|