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.
278 lines
9.3 KiB
278 lines
9.3 KiB
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)
|
|
|