anki-decks/generate.py

133 lines
5.1 KiB
Python
Executable file

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import click
import genanki
import markdown
import pathlib
import random
import re
RE_DIRNAME = re.compile(r'^(.*)\.([0-9]+)\.([0-9]+)$')
RE_FILENAME = re.compile(r'^(.*)\.([0-9]+)\.(md|html)$')
class MyNote(genanki.Note):
pass
def load_notes(
*,
path: pathlib.Path,
model: genanki.Model,
) -> list[genanki.Note]:
files_loaded = []
final_files: dict[str, list[tuple[pathlib.Path, int, bool]]] = {}
for dirpath, _, filenames in path.walk():
files_loaded += [dirpath / x for x in filenames]
for f in files_loaded:
match = RE_FILENAME.match(f.name)
if match:
final_files.setdefault(match.group(1), []).append(
(f, int(match.group(2)), match.group(3) == 'html'),
)
notes: list[genanki.Note] = []
for v in final_files.values():
note_files: list[tuple[pathlib.Path, bool]] = [(v_[0], v_[2]) for v_ in sorted(v, key=lambda x: x[1])]
note_str: list[str] = []
for f, is_html in note_files:
with f.open() as fp:
if is_html:
html_str = fp.read()
else:
html_str = markdown.markdown(fp.read())
note_str += [html_str]
notes += [MyNote(model=model, fields=note_str, guid=genanki.guid_for(note_str[0]))]
return notes
@click.command()
@click.option('--name', '-n', 'name', type=str, default=None)
@click.option('--model-name', '-m', 'model_name', type=str, default='Simple Model')
@click.option('--template-name', '-t', 'template_name', type=str, default='Card 1')
@click.option('--field', '-f', 'fields', type=str, default=['Question', 'Answer'], multiple=True)
@click.option('--qfmt', '-q', 'question_format', type=str, default='{{Question}}')
@click.option('--afmt', '-a', 'answer_format', type=str, default='{{FrontSide}}<hr id="answer">{{Answer}}')
@click.option('--model-id', 'model_id', type=int, default=None)
@click.option('--deck-id', 'deck_id', type=int, default=None)
@click.option('--destructive/--no-destructive', 'destructive', type=bool, default=False, help='Perform write operations, which may end in data loss')
@click.option('--autogenerate/--no-autogenerate', 'autogenerate', type=bool, default=False, help='Generate missing ids, should probably be used with --destructive, to always use the same ones.')
@click.option('--debug/--no-debug', 'debug', type=bool, default=False, help="Debug mode, don't write files")
@click.option('--multi', 'multi', type=click.Path(path_type=pathlib.Path))
@click.argument('paths', type=click.Path(path_type=pathlib.Path, dir_okay=True), nargs=-1)
def main(
*,
name: str | None,
model_name: str,
template_name: str,
fields: list[str],
question_format: str,
answer_format: str,
model_id: int | None,
deck_id: int | None,
destructive: bool,
autogenerate: bool,
debug: bool,
multi: pathlib.Path | None,
paths: list[pathlib.Path],
):
decks = []
for path in paths:
match = RE_DIRNAME.match(path.name)
if match:
model_name = model_name or match.group(1)
name = name or match.group(1)
model_id = model_id or int(match.group(2))
deck_id = deck_id or int(match.group(3))
else:
model_name = model_name or path.name
name = name or path.name
if model_id is None:
if autogenerate:
model_id = random.randrange(1 << 30, 1 << 31)
else:
raise click.ClickException('model_id could not be derived from directory name and is not explicitly set, or autogenerate enabled')
if deck_id is None:
if autogenerate:
deck_id = random.randrange(1 << 30, 1 << 31)
else:
raise click.ClickException('deck_id could not be derived from directory name and is not explicitly set, or autogenerate enabled')
if destructive:
new_path = path.parent / f'{path.name}.{model_id}.{deck_id}'
if not debug:
path.rename(new_path)
path = new_path
model = genanki.Model(
model_id,
model_name,
fields=[dict(name=x) for x in fields],
templates=[
{
'name': template_name,
'qfmt': question_format,
'afmt': answer_format,
},
],
)
notes: list[genanki.Note] = load_notes(path=path, model=model)
deck = genanki.Deck(
deck_id,
name,
)
for n in notes:
deck.add_note(n)
click.echo(f'added {len(notes)} notes')
decks += [deck]
if not debug:
genanki.Package(deck).write_to_file(path.parent / (path.name + '.apkg'))
if not debug and multi:
if multi.suffix != '.apkg':
click.echo('WARNING: destination file does not have extension ".apkg"')
genanki.Package(decks).write_to_file(multi)
if __name__ == '__main__':
main()