|
|
@ -4,6 +4,7 @@ import axios, { AxiosInstance } from 'axios'; |
|
|
|
|
|
|
|
import { soundTag } from './anki'; |
|
|
|
import { AnkiConnect, IAnkiConnectActions } from './ankiconnect'; |
|
|
|
import { ILevelLabel } from './level'; |
|
|
|
import { logger } from './logger'; |
|
|
|
|
|
|
|
export class WaniKani { |
|
|
@ -18,6 +19,48 @@ export class WaniKani { |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
static getLevelLabel(level: number): ILevelLabel { |
|
|
|
if (level < 11) { |
|
|
|
return { |
|
|
|
range: '01-10', |
|
|
|
ja: '快', |
|
|
|
en: 'PLEASANT', |
|
|
|
}; |
|
|
|
} else if (level < 21) { |
|
|
|
return { |
|
|
|
range: '11-20', |
|
|
|
ja: '苦', |
|
|
|
en: 'PAINFUL', |
|
|
|
}; |
|
|
|
} else if (level < 31) { |
|
|
|
return { |
|
|
|
range: '21-30', |
|
|
|
ja: '死', |
|
|
|
en: 'DEATH', |
|
|
|
}; |
|
|
|
} else if (level < 41) { |
|
|
|
return { |
|
|
|
range: '31-40', |
|
|
|
ja: '地獄', |
|
|
|
en: 'HELL', |
|
|
|
}; |
|
|
|
} else if (level < 51) { |
|
|
|
return { |
|
|
|
range: '41-50', |
|
|
|
ja: '天国', |
|
|
|
en: 'PARADISE', |
|
|
|
}; |
|
|
|
} else if (level < 61) { |
|
|
|
return { |
|
|
|
range: '51-60', |
|
|
|
ja: '現実', |
|
|
|
en: 'REALITY', |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
throw new Error(`invalid level: ${level}`); |
|
|
|
} |
|
|
|
|
|
|
|
async subjects({ |
|
|
|
force, |
|
|
|
}: { |
|
|
@ -239,6 +282,99 @@ export class WaniKani { |
|
|
|
}); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
async addTags( |
|
|
|
query: string, |
|
|
|
fields: { |
|
|
|
ja: string; |
|
|
|
}, |
|
|
|
) { |
|
|
|
const subjects = await this.subjects(); |
|
|
|
|
|
|
|
const vocabularies = subjects.filter( |
|
|
|
(s) => s.object === 'vocabulary', |
|
|
|
) as IVocabulary[]; |
|
|
|
const vocabMap = new Map< |
|
|
|
string, |
|
|
|
{ |
|
|
|
level: number; |
|
|
|
label: ILevelLabel; |
|
|
|
} |
|
|
|
>(); |
|
|
|
vocabularies.map((v) => { |
|
|
|
if (vocabMap.has(v.data.characters)) return; |
|
|
|
vocabMap.set(v.data.characters, { |
|
|
|
level: v.data.level, |
|
|
|
label: WaniKani.getLevelLabel(v.data.level), |
|
|
|
}); |
|
|
|
}); |
|
|
|
if (!vocabMap.size) return; |
|
|
|
|
|
|
|
query += ` -${fields.ja}:`; |
|
|
|
|
|
|
|
const anki = new AnkiConnect(); |
|
|
|
anki |
|
|
|
.api('findNotes', { |
|
|
|
query, |
|
|
|
}) |
|
|
|
.then((notes) => anki.api('notesInfo', { notes })) |
|
|
|
.then(async (notes) => { |
|
|
|
const notesToUpdate: { |
|
|
|
id: SubjectID; |
|
|
|
tags: string[]; |
|
|
|
}[] = []; |
|
|
|
|
|
|
|
for (const n of notes) { |
|
|
|
const { value: ja } = n.fields[fields.ja] || {}; |
|
|
|
if (ja) { |
|
|
|
const vocab = vocabMap.get( |
|
|
|
ja.replace(/\[.+?\]/g, '').replace(/ /g, ''), |
|
|
|
); |
|
|
|
if (vocab) { |
|
|
|
notesToUpdate.push({ |
|
|
|
id: n.noteId, |
|
|
|
tags: [ |
|
|
|
'wanikani', |
|
|
|
`level-${vocab.level}`, |
|
|
|
`wanikani-level-${vocab.level}`, |
|
|
|
`wanikani-level-${vocab.label.range}-${vocab.label.ja}`, |
|
|
|
`wanikani-level-${ |
|
|
|
vocab.label.range |
|
|
|
}-${vocab.label.en.toLocaleLowerCase()}`,
|
|
|
|
], |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (!notesToUpdate.length) return; |
|
|
|
|
|
|
|
const addTagsMap = new Map< |
|
|
|
string, |
|
|
|
{ |
|
|
|
notes: SubjectID[]; |
|
|
|
tags: string; |
|
|
|
} |
|
|
|
>(); |
|
|
|
|
|
|
|
notesToUpdate.map(({ id, tags }) => { |
|
|
|
const identifier = JSON.stringify(tags); |
|
|
|
const v = addTagsMap.get(identifier) || { |
|
|
|
notes: [], |
|
|
|
tags: tags.join(' '), |
|
|
|
}; |
|
|
|
v.notes.push(id); |
|
|
|
addTagsMap.set(identifier, v); |
|
|
|
}); |
|
|
|
|
|
|
|
await anki.multi<'addTags'[]>({ |
|
|
|
actions: Array.from(addTagsMap.values()).map((params) => ({ |
|
|
|
action: 'addTags', |
|
|
|
params, |
|
|
|
})), |
|
|
|
}); |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
type WaniKaniDate = string; |
|
|
|