import os.path import re import json import urllib.request from aqt import mw from anki.sound import allSounds from anki.utils import stripHTMLMedia from aqt.qt import QAction from collections import namedtuple def reshow_card(): if mw.state != "review": return None card = mw.reviewer.card if not card: return None ordinal = card.ord note = card.note(reload=True) matching_cards = [card for card in note.cards() if card.ord == ordinal] if len(matching_cards) == 1: mw.reviewer.cardQueue.append(matching_cards[0]) mw.reset() def current_note_in_review(): if mw.state != "review": return None card = mw.reviewer.card if not card: return None return card.note(reload=True) GOOD_CHARACTERS = re.compile("^[A-Za-z0-9_\- ]+\.[A-Za-z0-9_\- ]+$") SOUND_GOOD = "Sound" SOUND_BAD = "SoundBroken" def toggle_sound(): note = current_note_in_review() if not note or SOUND_GOOD not in note or SOUND_BAD not in note: return mediadir = mw.col.media.dir() def handle_filename(filename): fullpath = os.path.join(mediadir, filename) if not os.path.exists(fullpath): return filename, "File not found" # if not GOOD_CHARACTERS.match(filename): # data = open(fullpath, "rb") # basename, extension = os.path.splitext(filename) # new_filename = "note%s-%s%s" % (note.id, base64.urlsafe_b64encode(basename), extension) # new_filename = mw.col.media.writeData(opath=new_filename, data=data) # return new_filename, None return filename, None good_sounds = allSounds(note[SOUND_GOOD]) bad_sounds = allSounds(note[SOUND_BAD]) if len(good_sounds) == 1: sound = good_sounds[0] elif len(bad_sounds) == 1: sound = bad_sounds[0] else: sound = None if not sound: return new_sound, error = handle_filename(sound) if error: note[SOUND_GOOD] = "" note[SOUND_BAD] = "[sound %s] (%s)" % (new_sound, error) else: note[SOUND_GOOD] = "[sound %s]" % (new_sound,) note[SOUND_BAD] = "" note.flush() reshow_card() # copied from https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Python def levenshtein(s1, s2): if len(s1) < len(s2): return levenshtein(s2, s1) # len(s1) >= len(s2) if len(s2) == 0: return len(s1) previous_row = range(len(s2) + 1) for i, c1 in enumerate(s1): current_row = [i + 1] for j, c2 in enumerate(s2): insertions = previous_row[ j + 1] + 1 # j+1 instead of j since previous_row and current_row are one character longer deletions = current_row[j] + 1 # than s2 substitutions = previous_row[j] + (c1 != c2) current_row.append(min(insertions, deletions, substitutions)) previous_row = current_row return previous_row[-1] PRACTICE_SENTENCE = "practice sentence" PRACTICE_SENTENCE_THRESHOLD = 6 MEANING = "Meaning" EXTRA_INFO = "ExtraInfo" def swap_meaning_and_extra_info(): note = current_note_in_review() if not note or MEANING not in note or EXTRA_INFO not in note: return meaning = note[MEANING] extra_info = note[EXTRA_INFO] if levenshtein(stripHTMLMedia(meaning), PRACTICE_SENTENCE) < 5: # PRACTICE SENTENCE!!!! note[MEANING] = extra_info note[EXTRA_INFO] = "" else: note[MEANING] = extra_info note[EXTRA_INFO] = meaning note.flush() reshow_card() def wrap_list_and_add(owner, name, items): items = list(items) old = getattr(owner, name) def new_func(*args, **kwargs): return old(*args, **kwargs) + items setattr(owner, name, new_func) class Config(object): __slots__ = ["wk_api_key", "kanji_query", "kanji_list_cache"] @classmethod def from_config(cls): return cls.from_json(mw.addonManager.getConfig(__name__)) @classmethod def from_json(cls, json_obj): return Config(wk_api_key=json_obj.get("wk_api_key", ""), kanji_query=json_obj.get("kanji_query", ""), kanji_list_cache=KanjiListCache.from_json(json_obj.get("kanji_list_cache", None))) def __init__(self, wk_api_key="", kanji_query="", kanji_list_cache=None): self.wk_api_key = wk_api_key self.kanji_query = kanji_query self.kanji_list_cache = kanji_list_cache or KanjiListCache() def save(self): mw.addonManager.writeConfig(__name__, { "wk_api_key": self.wk_api_key, "kanji_query": self.kanji_query, "kanji_list_cache": self.kanji_list_cache.to_json(), }) class KanjiListCache(object): __slots__ = ["definitions", "last_list_etag", "last_list_update", "last_definition_etag", "last_definition_update"] @classmethod def from_json(cls, json_obj=None): if not json_obj: return def __init__(self, definitions=None, last_list_etag="", last_list_update="", last_definition_etag="", last_definition_update=""): self.definitions = dict(definitions or {}) self.last_list_etag = last_list_etag self.last_list_update = last_list_update self.last_definition_etag = last_definition_etag self.last_definition_update = last_definition_update def to_json(self): return { "definitions": self.definitions, "last_definition_etag": self.last_definition_etag, "last_definition_update": self.last_definition_update, "last_list_etag": self.last_list_etag, "last_list_update": self.last_list_update} def update_unlocked_kanji(config): url = ( r"https://api.wanikani.com/v2/assignments" r"?started=true" r"&subject_types=kanji") if config.kanji_list_cache.last_list_update: url += r"&updated_after=" + config.kanji_list_cache.last_list_update list_etag = None list_updated = None ids = set() while url: list_request = urllib.request.Request(url) list_request.add_header("Authorization", "Bearer {wk_api_key}".format(wk_api_key=config.wk_api_key)) list_request.add_header("Wanikani-Revision", "20170710") if config.kanji_list_cache.last_list_etag: list_request.add_header("If-None-Match", config.kanji_list_cache.last_list_etag) with urllib.request.urlopen(list_request) as result: code = result.getcode() headers = result.info() page = json.loads(result.read()) if code != 200: raise ValueError("Got %s from list request %s: %s" % (code, url, json.dumps(page))) list_etag = headers["ETag"] list_updated = page["data_updated_at"] ids.add(item["id"] for item in page["data"]) if page["pages"]["next_page"]: url = page["pages"]["next_page"] else: url = None current_ids = set(int(cached_id) for cached_id in config.kanji_list_cache.definitions.keys()) all_ids = ids | current_ids # TODO: Get the updated kanji definitions, patch them in to the current dict # Retrieve any items corresponding to ids which are not present in the cache without any cache specifiers # Update the four cache specifiers # Save the configuration def get_updated_kanji_definitions(config, all_ids): url = ( r"https://api.wanikani.com/v2/subjects" r"?types=kanji" r"&ids=%s" % (",".join(all_ids)) ) if config.kanji_list_cache.last_definition_update: url += r"&updated_after=" + config.kanji_list_cache.last_definition_update etag = None updated = None updated_definitions = {} while url: list_request = urllib.request.Request(url) list_request.add_header("Authorization", "Bearer {wk_api_key}".format(wk_api_key=config.wk_api_key)) list_request.add_header("Wanikani-Revision", "20170710") if config.kanji_list_cache.last_definition_etag: list_request.add_header("If-None-Match", config.kanji_list_cache.last_definition_etag) with urllib.request.urlopen(list_request) as result: code = result.getcode() headers = result.info() page = json.loads(result.read()) if code != 200: raise ValueError("Got %s from definition request %s: %s" % (code, url, json.dumps(page))) etag = headers["ETag"] updated = page["data_updated_at"] for item in page["data"]: updated_definitions[item["id"]] = item["characters"] if page["pages"]["next_page"]: url = page["pages"]["next_page"] else: url = None return updated, etag, updated_definitions def sync_wani_kani(): pass new_shortcuts = ( ["Shift+s", toggle_sound], ["Shift+m", swap_meaning_and_extra_info], ) new_context_menu_items = ( None, ["Toggle Sound", "Shift+S", toggle_sound], ["Swap Meaning and Extra Info", "Shift+M", swap_meaning_and_extra_info], ) wrap_list_and_add(mw.reviewer, "_shortcutKeys", new_shortcuts) wrap_list_and_add(mw.reviewer, "_contextMenu", new_context_menu_items) action = QAction("Load WaniKani data", mw) action.triggered.connect(sync_wani_kani) mw.form.menuTools.addAction(action)