import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
|
|
import axios, { AxiosInstance } from 'axios';
|
|
|
|
export class WaniKani {
|
|
$axios: AxiosInstance;
|
|
|
|
constructor(public apiKey = process.env['WANIKANI_API_KEY']!) {
|
|
this.$axios = axios.create({
|
|
baseURL: 'https://api.wanikani.com/v2/',
|
|
headers: {
|
|
Authorization: `Bearer ${process.env['WANIKANI_API_KEY']}`,
|
|
},
|
|
});
|
|
}
|
|
|
|
async subjects({
|
|
force,
|
|
}: {
|
|
force?: boolean;
|
|
} = {}) {
|
|
const subjects = {
|
|
data: [] as ISubject[],
|
|
filename: 'cache/wanikani.json',
|
|
load() {
|
|
this.data = existsSync(this.filename)
|
|
? JSON.parse(readFileSync(this.filename, 'utf-8'))
|
|
: [];
|
|
return this.data;
|
|
},
|
|
dump(d: ISubject[]) {
|
|
this.data = d;
|
|
this.finalize();
|
|
},
|
|
finalize() {
|
|
writeFileSync(this.filename, JSON.stringify(this.data));
|
|
},
|
|
};
|
|
|
|
let data = subjects.load();
|
|
if (data.length && !force) {
|
|
return data;
|
|
}
|
|
data = [];
|
|
|
|
let nextURL = '/subjects';
|
|
while (nextURL) {
|
|
const r = await this.$axios
|
|
.get<{
|
|
pages: {
|
|
next_url?: string;
|
|
};
|
|
data: ISubject[];
|
|
}>(nextURL)
|
|
.then((r) => r.data);
|
|
data.push(...r.data);
|
|
console.info(r.pages.next_url);
|
|
nextURL = r.pages.next_url || '';
|
|
}
|
|
|
|
subjects.dump(data);
|
|
return data;
|
|
}
|
|
}
|
|
|
|
type WaniKaniDate = string;
|
|
type Integer = number;
|
|
type Level = Integer;
|
|
type SubjectID = Integer;
|
|
|
|
interface IMeaning {
|
|
meaning: string;
|
|
primary: boolean;
|
|
}
|
|
|
|
interface ISubjectBase<
|
|
T extends 'radical' | 'kanji' | 'vocabulary',
|
|
Data extends {},
|
|
> {
|
|
id: SubjectID;
|
|
object: T;
|
|
url: string;
|
|
data_updated_at: WaniKaniDate;
|
|
data: {
|
|
auxillary_meanings: IMeaning[];
|
|
characters: string;
|
|
created_at: WaniKaniDate;
|
|
document_url: string;
|
|
hidden_at: WaniKaniDate | null;
|
|
lesson_position: Integer;
|
|
level: Level;
|
|
meaning_mnemonic: string;
|
|
meanings: IMeaning[];
|
|
slug: string;
|
|
spaced_repetition_system_id: Integer;
|
|
} & Data;
|
|
}
|
|
|
|
export type IRadical = ISubjectBase<
|
|
'radical',
|
|
{
|
|
character_images: {
|
|
url: string;
|
|
metadata: {
|
|
inline_styles?: boolean;
|
|
dimensions?: string;
|
|
};
|
|
content_type: string;
|
|
}[];
|
|
amalgamation_subject_ids: SubjectID[];
|
|
}
|
|
>;
|
|
|
|
export type IKanji = ISubjectBase<
|
|
'kanji',
|
|
{
|
|
readings: {
|
|
reading: string;
|
|
primary: boolean;
|
|
type: 'kunyomi' | 'onyomi';
|
|
}[];
|
|
component_subject_ids: SubjectID[];
|
|
visually_similar_subject_ids: SubjectID[];
|
|
}
|
|
>;
|
|
|
|
export type IVocabulary = ISubjectBase<
|
|
'vocabulary',
|
|
{
|
|
component_subject_ids: SubjectID[];
|
|
context_sentences: {
|
|
en: string;
|
|
ja: string;
|
|
}[];
|
|
part_of_speech: string[];
|
|
pronunciation_audios: {
|
|
url: string;
|
|
metadata: {
|
|
gender: 'male' | 'female';
|
|
source_id: Integer;
|
|
pronunciation: string;
|
|
voice_actor_id: Integer;
|
|
voice_actor_name: string;
|
|
voice_description: string;
|
|
};
|
|
content_type: 'audio/mpeg' | 'audio/ogg';
|
|
}[];
|
|
readings: {
|
|
accepted_answer: boolean;
|
|
primary: boolean;
|
|
reading: string;
|
|
}[];
|
|
reading_mnemonic: string;
|
|
}
|
|
>;
|
|
|
|
export type ISubject = IRadical | IKanji | IVocabulary;
|