From 1f8cb044ae6dc52db07b682a1f48f8b56d2f4b70 Mon Sep 17 00:00:00 2001 From: Dustin Swan Date: Sun, 21 Mar 2021 00:42:12 -0400 Subject: [PATCH] Something kind of working for this WaniKani cli app --- index.ts | 194 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 index.ts diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..c06174d --- /dev/null +++ b/index.ts @@ -0,0 +1,194 @@ +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(); + +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 => { + 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 => { + 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();