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

2 years ago
2 years ago
2 years ago
  1. import { AnkiConnect, IAnkiConnectActions } from '@/ankiconnect';
  2. import { makeKanjiLevels } from './get-kanji-level';
  3. const DECK = 'Takoboto';
  4. const KANJI_FIELD = 'Japanese';
  5. /**
  6. * Whether to use Level > 60, i.e. outside WaniKani
  7. *
  8. * @link https://community.wanikani.com/t/fake-levels-61-70-or-%E7%84%A1%E9%99%90-infinity/16399
  9. */
  10. const USE_BEYOND = true;
  11. async function main() {
  12. const anki = new AnkiConnect();
  13. const deckConfig = await anki.api('getDeckConfig', {
  14. deck: DECK,
  15. });
  16. const deckQuery = `deck:${DECK} -deck:${DECK}::*`;
  17. const kanjiLevels = await makeKanjiLevels({ useBeyond: USE_BEYOND });
  18. await anki
  19. .api('findCards', {
  20. query: deckQuery,
  21. })
  22. .then((cards) => anki.api('cardsInfo', { cards }))
  23. .then(async (rs) => {
  24. const modelToSubdecks = new Map<
  25. string,
  26. {
  27. [subdeck: string]: number[];
  28. }
  29. >();
  30. rs.map((r) => {
  31. const vs = modelToSubdecks.get(r.modelName) || {};
  32. const { value } = r.fields[KANJI_FIELD] || {};
  33. if (value) {
  34. // Get the subdeck names from Kanji
  35. let subdeck = '';
  36. let level = 0;
  37. Array.from(value).map((k) => {
  38. const m = kanjiLevels.get(k);
  39. if (m && m.level > level) {
  40. level = m.level;
  41. subdeck = m.deckName;
  42. }
  43. });
  44. if (subdeck) {
  45. const cardIds = vs[subdeck] || [];
  46. cardIds.push(r.cardId);
  47. vs[subdeck] = cardIds;
  48. modelToSubdecks.set(r.modelName, vs);
  49. }
  50. }
  51. });
  52. return Promise.all(
  53. Array.from(modelToSubdecks).map(([modelName, subdecks]) =>
  54. anki.api('modelTemplates', { modelName }).then((templates) =>
  55. Object.entries(subdecks).map(([subdeck, cardIds]) => ({
  56. modelName,
  57. templateNames: Object.keys(templates),
  58. subdeck,
  59. cardIdsSet: new Set(cardIds),
  60. })),
  61. ),
  62. ),
  63. );
  64. })
  65. .then(async (rs) => {
  66. const remaining: {
  67. [templateName: string]: Set<number>;
  68. } = {};
  69. const cardsByTemplateName = {
  70. data: new Map<string, number[]>(),
  71. async get(templateName: string) {
  72. let v = this.data.get(templateName);
  73. if (!v) {
  74. v = await anki.api('findCards', {
  75. query: `${deckQuery} card:${templateName}`,
  76. });
  77. remaining[templateName] = new Set(v);
  78. }
  79. this.data.set(templateName, v);
  80. return v;
  81. },
  82. };
  83. const toChangeDeck: IAnkiConnectActions['changeDeck']['params'][] = [];
  84. for (const { templateName, subdeck, cardIdsSet } of rs
  85. .flat()
  86. .flatMap((r) =>
  87. r.templateNames.map((templateName) => ({
  88. ...r,
  89. templateName,
  90. })),
  91. )) {
  92. const cards = await cardsByTemplateName.get(templateName);
  93. toChangeDeck.push({
  94. cards: cards.filter((c) => {
  95. const r = remaining[templateName];
  96. if (r) {
  97. r.delete(c);
  98. }
  99. return cardIdsSet.has(c);
  100. }),
  101. deck: `${DECK}::${subdeck}::${templateName}`,
  102. });
  103. }
  104. Object.entries(remaining).map(([templateName, cards]) => {
  105. if (cards.size) {
  106. toChangeDeck.push({
  107. cards: [...cards],
  108. deck: `${DECK}::独習::${templateName}`,
  109. });
  110. }
  111. });
  112. if (toChangeDeck.length) {
  113. const batchSize = 20;
  114. for (let i = 0; i < toChangeDeck.length; i += batchSize) {
  115. await Promise.all(
  116. toChangeDeck
  117. .slice(i, i + batchSize)
  118. .map((params) => anki.api('changeDeck', params)),
  119. );
  120. }
  121. await anki.api('setDeckConfigId', {
  122. decks: toChangeDeck.map((r) => r.deck),
  123. configId: deckConfig.id,
  124. });
  125. }
  126. });
  127. }
  128. if (require.main === module) {
  129. main();
  130. }