WaniKani CLI App
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.
 

194 lines
5.3 KiB

import { LocalStorage } from "https://deno.land/x/storage@0.0.5/mod.ts";
import * as Colors from "https://deno.land/std/fmt/colors.ts";
import wanakana from 'https://cdn.esm.sh/v27/wanakana@4.0.2/esnext/wanakana.js';
import { readKeypress } from "https://deno.land/x/keypress@0.0.7/mod.ts";
import { Select, Input, Confirm } from "https://deno.land/x/cliffy/prompt/mod.ts";
const API_BASE = "https://api.wanikani.com/v2";
const store = new LocalStorage<string>();
async function getToken() {
if (!store.get('token')) {
store.set({ token: await Input.prompt('Enter your API Token') });
store.save();
}
}
const wkFetch = (url: string, options = {}) =>
fetch(url, {
...options,
headers: {
"Wanikani-Revision": "20170710",
Authorization: `Bearer ${store.get("token")}`,
},
});
const subjects = JSON.parse(store.get('subjects') || '{}');
const getSubject = async (id: number) => {
if (!subjects[id]) {
subjects[id] = await wkFetch(`${API_BASE}/subjects/${id}`).then((res) => res.json());
store.set('subjects', JSON.stringify(subjects));
store.save();
}
return subjects[id];
}
const getSummary = async () =>
await wkFetch(`${API_BASE}/summary`)
.then((res) => {
if (res.ok) {
return res.json();
} else {
throw "Error";
}
})
function authError() {
console.error('Error authenticating.');
store.delete('token');
}
// const availableReviews = (summary: any) =>
const availableNow = (type: string, summary: { data: any }) =>
summary.data[type].filter((rev: any) => {
const date = new Date(rev.available_at);
return date < new Date();
}).flatMap((e: { subject_ids: any }) => e.subject_ids);
async function loop(summary: { data: any }) {
const reviews = availableNow("reviews", summary);
const lessons = availableNow("lessons", summary);
console.log("W A N I 🦀 🐊 K A N I");
console.log(Colors.red(lessons.length.toString() + " Lessons") + ' ' + Colors.blue(reviews.length.toString() + ' Reviews'));
const command: string = await Select.prompt({
message: "What would you like to do?",
options: [
{ name: "Reviews", value: "reviews" },
{ name: "Lessons", value: "lessons", disabled: true },
Select.separator("--------"),
{ name: "Quit", value: "quit" },
],
});
switch (command) {
case "quit":
break;
case "reviews":
await startReviews(reviews);
loop(summary);
break;
default:
loop(summary);
}
}
const startReviews = async (reviews: any) => {
const [first, ...rest] = reviews;
const subject = await getSubject(first);
try {
if (subject.object !== 'radical') {
const incorrectReadings = await readingPrompt(subject);
const audio = subject.data.pronunciation_audios?.[0].url;
if (audio) {
const cmd = Deno.run({ cmd: ["mpv", audio], stdout: "null", stderr: "null" }); // TODO
await cmd.status();
cmd.close();
}
if (!incorrectReadings) {
console.log("Correct!");
}
}
const incorrectMeanings = await meaningPrompt(subject);
if (!incorrectMeanings) {
console.log("Correct!");
}
} catch(e) {
console.log('error', e);
// TODO wrap up
// loop(summary);
return;
}
await startReviews(rest);
}
const readingPrompt = async (subject: any): Promise<number> => {
const { object, data: { characters, readings } } = subject;
console.log(`${characters} (${object} reading)`);
let input = '';
for await (const key of readKeypress()) {
if (key.key === 'return') {
break;
} else if (key.ctrlKey && key.key === 'c') {
throw 'Cancelled';
} else if (key.key === 'backspace') {
input = input.slice(0, -1);
} else {
input = input + key.sequence;
}
const kana = wanakana.toHiragana(input);
const text = new TextEncoder().encode('\r' + kana);
Deno.writeAll(Deno.stdout, text);
}
const reading = wanakana.toHiragana(input);
const text = new TextEncoder().encode('\r\n');
Deno.writeAll(Deno.stdout, text);
if (readings.find((r: any) => r.reading === reading && r.accepted_answer)) {
return 0;
} else if (readings.find((r: any) => r.reading === reading && !r.accepted_answer)) {
console.log('Looking for the other reading...');
return await readingPrompt(subject);
} else {
const tryAgain: boolean = await Confirm.prompt({ message: "Try again?", default: true });
if (!tryAgain) {
console.log(readings);
return 1;
} else {
return 1 + await readingPrompt(subject);
}
}
};
const meaningPrompt = async (subject: any): Promise<number> => {
const { object, data: { characters, meanings } } = subject;
console.log(`${characters} (${object} meaning)`);
const meaning = await Input.prompt('');
if (meanings.find((r: any) => r.meaning.toLowerCase() === meaning.toLowerCase() && r.accepted_answer)) {
return 0;
} else {
const tryAgain: boolean = await Confirm.prompt({ message: "Try again?", default: true });
if (!tryAgain) {
console.log(meanings);
return 1;
} else {
return 1 + await meaningPrompt(subject);
}
}
};
async function start() {
await getToken();
try {
const summary = await getSummary();
loop(summary);
} catch(e) {
authError();
start();
}
}
start();