From f05ee8709f26c36561a688facd687186e597702b Mon Sep 17 00:00:00 2001 From: Adrian Heine Date: Mon, 8 Apr 2024 11:38:56 +0200 Subject: [PATCH] Init --- debian/changelog | 5 ++ debian/control | 17 ++++ debian/copyright | 6 ++ debian/klette.install | 2 + debian/rules | 4 + debian/source/format | 1 + main.py | 199 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 234 insertions(+) create mode 100644 debian/changelog create mode 100644 debian/control create mode 100644 debian/copyright create mode 100755 debian/klette.install create mode 100755 debian/rules create mode 100644 debian/source/format create mode 100644 main.py diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..f0b87d6 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +klette (0.1) UNRELEASED; urgency=low + + * Initial release. + + -- Adrian Heine Mon, 08 Apr 2024 11:29:30 +0200 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..8f06bd9 --- /dev/null +++ b/debian/control @@ -0,0 +1,17 @@ +Source: klette +Section: education +Priority: optional +Maintainer: Adrian Heine +Build-Depends: debhelper-compat (= 13), dh-python, python3-all, dh-exec +Standards-Version: 4.6.2 +Homepage: https://git.adrianheine.de/adrian/klette +Rules-Requires-Root: no +Vcs-Git: https://git.adrianheine.de/adrian/klette.git +Vcs-Browser: https://git.adrianheine.de/adrian/klette +X-Python3-Version: >= 3.10 + +Package: klette +Architecture: all +Multi-Arch: foreign +Depends: ${misc:Depends}, ${python3:Depends}, python3-pyamf +Description: Replacement for klett's flash-based vocabulary trainer diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..2b55dc2 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,6 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: klette + +Files: * +Copyright: 2024 Adrian Heine +License: AGPL-3.0+ diff --git a/debian/klette.install b/debian/klette.install new file mode 100755 index 0000000..a086f30 --- /dev/null +++ b/debian/klette.install @@ -0,0 +1,2 @@ +#!/usr/bin/dh-exec +main.py => /usr/bin/klette diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..25115de --- /dev/null +++ b/debian/rules @@ -0,0 +1,4 @@ +#!/usr/bin/make -f + +%: + dh $@ --with python3 diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/main.py b/main.py new file mode 100644 index 0000000..1aaa6a0 --- /dev/null +++ b/main.py @@ -0,0 +1,199 @@ +#!/usr/bin/python3 + +import pyamf +import shutil +import json +import os +import argparse +import unicodedata +import random +import re +import sys +import readline + +DATA_DIR = os.environ['XDG_DATA_HOME'] + '/klette' +VOKABELN_FILE = DATA_DIR + '/vokabeln.json' +STATUS_FILE = DATA_DIR + '/status.json' +AUDIO_BASE = DATA_DIR + '/audio/' + +class bcolors: + HEADER = '\033[95m' + OKBLUE = '\033[94m' + OKCYAN = '\033[96m' + OKGREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + LINK_START = '\033]8;;' + LINK_MIDDLE = '\033\\' + LINK_END = '\033]8;;\033\\' + +def import_vokabeln(file_name, audio_base): + os.makedirs(AUDIO_BASE, exist_ok=True) + palabras = [] + with open(file_name, 'rb') as reader: + obj = pyamf.decode(reader.read()) + for unidad_data in obj.readElement()['lektionList']: + unidad = unidad_data['name'] + for paso_data in unidad_data['lektionsTeilList']: + paso = paso_data['name'] + for palabra in paso_data['entryList']: + audio = palabra['vorderSeite']['swTonAbgleichwort'] + palabras.append({ + 'id': palabra['matId'], + 'de': palabra['rueckSeite']['text'], + 'es': palabra['vorderSeite']['text'], + 'audio': audio, + 'unidad': unidad, + 'paso': paso + }) + shutil.copy2(audio_base + audio + '.bin', + AUDIO_BASE + audio + '.aac') + with open(VOKABELN_FILE, 'x') as writer: + writer.write(json.dumps(palabras)) + +def import_status(file_name): + os.makedirs(DATA_DIR, exist_ok=True) + statuses = {} + with open(file_name, 'rb') as reader: + obj = pyamf.decode(reader.read()).readElement()[0] + for idx in obj: + item = obj[idx] + statuses[item['matId']] = item['statusWortschatz'] + with open(STATUS_FILE, 'x') as writer: + writer.write(json.dumps(statuses)) + +def vergleiche(respuesta, expecta): + if respuesta == "": return 4 + if respuesta[0] == '!' and expecta[0] == '¡': + respuesta = '¡' + respuesta[1:] + if respuesta[0] == '?' and expecta[0] == '¿': + respuesta = '¿' + respuesta[1:] + respuesta = unicodedata.normalize('NFD', respuesta) + expecta = unicodedata.normalize('NFD', expecta) + if respuesta == expecta: return 0 + respuesta = respuesta.encode('ASCII', 'ignore').decode('ASCII') + expecta = expecta.encode('ASCII', 'ignore').decode('ASCII') + if respuesta == expecta: return 1 + respuesta = respuesta.casefold() + expecta = expecta.casefold() + if respuesta == expecta: return 2 + respuesta = re.sub('\W', '', respuesta) + expecta = re.sub('\W', '', expecta) + if respuesta == expecta: return 3 + return 4 + +assert vergleiche("e", "e") == 0 +assert vergleiche("é", "e") == 1 +assert vergleiche("Bien", "bien") == 2 +assert vergleiche("quétal", "Qué tal") == 3 +assert vergleiche("Buenas Noches", "¡Buenas noches!") == 3 + +class Resultado: + BIEN = 1 + MAL = 2 + ADIOS = 3 + +def einzelne_abfrage(palabra, status): + status_text = f"({status})" if status > 0 else f"{bcolors.OKGREEN}neu{bcolors.ENDC}" + try: + respuesta = input(f"{status_text} {bcolors.BOLD}{palabra['de']}{bcolors.ENDC} {bcolors.OKCYAN}") + except EOFError: + print() + return Resultado.ADIOS + finally: + print(bcolors.ENDC, end="") + print('\033[{}C\033[1A'.format(4 + len(palabra['de']) + 1 + len(respuesta)), end="") + resultado = vergleiche(respuesta, palabra['es']) + match status: + case 0: bien = resultado < 4 + case 1: bien = resultado < 3 + case 2: bien = resultado < 3 + case 3: bien = resultado < 2 + case 4: bien = resultado < 1 + print(f" {bcolors.LINK_START}file://{AUDIO_BASE}{palabra['audio']}.aac{bcolors.LINK_MIDDLE}",end="") + if bien and resultado == 0: + print(f"{bcolors.OKGREEN}✓{bcolors.ENDC}",end="") + elif bien: + print(f"{bcolors.WARNING}{palabra['es']}{bcolors.ENDC}",end="") + else: + print(f"{bcolors.FAIL}{palabra['es']}{bcolors.ENDC}",end="") + print(bcolors.LINK_END,end="") + try: + input(" ") + except EOFError: + print() + return Resultado.ADIOS + return Resultado.BIEN if bien else Resultado.MAL + +def abfrage(parser): + random.seed() + try: + with open(VOKABELN_FILE, 'r') as f: + palabras = json.load(f) + with open(STATUS_FILE, 'r') as f: + status = json.load(f) + except FileNotFoundError: + print(f"{bcolors.FAIL}Daten können nicht geladen werden, hast du sie schon importiert?{bcolors.ENDC}") + print() + parser.print_help() + return + + unidad = False + paso = False + cur_palabras = False + bien = 0 + mal = 0 + for palabra in palabras: + if paso != palabra['paso']: + if cur_palabras != False: + c = [len(cur_palabras[x]) for x in cur_palabras] + print(f"{bcolors.BOLD}{unidad}{bcolors.ENDC}: {paso} (",end="") + print(*c, sep="/", end=")") + print() + for n in range(5): # 1..4, no 5 + random.shuffle(cur_palabras[n]) + for palabra in cur_palabras[n]: + s = status.get(palabra['id'], 0) + match einzelne_abfrage(palabra, s): + case Resultado.BIEN: + status[palabra['id']] = s + 1 + bien+=1 + case Resultado.MAL: + if s > 0: + status[palabra['id']] = s - 1 + mal+=1 + continue + case Resultado.ADIOS: + print(f'{bcolors.OKGREEN}+{bien}{bcolors.ENDC} / {bcolors.FAIL}-{mal}{bcolors.ENDC}') + return + with open(STATUS_FILE + '.new', 'w') as f: + json.dump(status, f) + f.flush() + os.fsync(f.fileno()) + with open(STATUS_FILE + '.new', 'r') as f: + status = json.load(f) + os.replace(STATUS_FILE + '.new', STATUS_FILE) + + cur_palabras = {0: [], 1: [], 2: [], 3: [], 4: [], 5: []} + unidad = palabra['unidad'] + paso = palabra['paso'] + + cur_palabras[status.get(palabra['id'], 0)].append(palabra) + +parser = argparse.ArgumentParser() +default_data_file = os.environ['PWD'] + '/assets/amf/vokabelTrainer147.amf' +parser.add_argument('--import-data', type=str, help="Path to assets", metavar="DIR") +default_status_file = os.environ['HOME'] + '/klett/1266/vokabeltrainerData147' +parser.add_argument('--import-status', type=str, help="Path to AMF File, defaults to " + default_status_file, metavar="FILE", nargs='?', const=default_status_file) +args = parser.parse_args() + +if args.import_data: + import_vokabeln(args.import_data + '/amf/vokabelTrainer147.amf', + args.import_data + '/amf/medium/') +elif args.import_status: + import_status(args.import_status) +else: + abfrage(parser)