From 4ab571236076d4a5970e1a0a2db71dd7a7ed7270 Mon Sep 17 00:00:00 2001 From: arne314 <73391160+arne314@users.noreply.github.com> Date: Sat, 25 Jan 2025 16:10:13 +0100 Subject: [PATCH] feat(anki): reimport flashcards --- README.md | 3 +++ lua/typstar/anki.lua | 6 ++++++ lua/typstar/init.lua | 3 +++ src/anki/anki_api.py | 27 +++++++++++++++++++++++---- src/anki/flashcard.py | 2 +- src/anki/main.py | 19 ++++++++++++++++--- 6 files changed, 52 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b436113..8b266a6 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,9 @@ To render the flashcard in your document as well add some code like this - Use `:TypstarAnkiScan` to scan the current nvim working directory and compile all flashcards in its context, unchanged files will be ignored - Use `:TypstarAnkiForce` to force compilation of all flashcards in the current working directory even if the files haven't changed since the last scan (e.g. on preamble change) - Use `:TypstarAnkiForceCurrent` to force compilation of all flashcards in the file currently edited +- Use `:TypstarAnkiReimport` to also add flashcards that have already been asigned an id but are not currently +present in Anki +- Use `:TypstarAnkiForceReimport` and `:TypstarAnkiForceCurrentReimport` to combine features accordingly #### Standalone - Run `typstar-anki --help` to show the available options diff --git a/lua/typstar/anki.lua b/lua/typstar/anki.lua index d0d3490..9dfa6e6 100644 --- a/lua/typstar/anki.lua +++ b/lua/typstar/anki.lua @@ -22,8 +22,14 @@ end function M.scan() run_typstar_anki('') end +function M.scan_reimport() run_typstar_anki('--reimport') end + function M.scan_force() run_typstar_anki('--force-scan ' .. vim.fn.getcwd()) end +function M.scan_force_reimport() run_typstar_anki('--reimport --force-scan ' .. vim.fn.getcwd()) end + function M.scan_force_current() run_typstar_anki('--force-scan ' .. vim.fn.expand('%:p')) end +function M.scan_force_current_reimport() run_typstar_anki('--reimport --force-scan ' .. vim.fn.expand('%:p')) end + return M diff --git a/lua/typstar/init.lua b/lua/typstar/init.lua index 6089e82..e0c49eb 100644 --- a/lua/typstar/init.lua +++ b/lua/typstar/init.lua @@ -14,8 +14,11 @@ M.setup = function(args) vim.api.nvim_create_user_command('TypstarOpenExcalidraw', excalidraw.open_drawing, {}) vim.api.nvim_create_user_command('TypstarAnkiScan', anki.scan, {}) + vim.api.nvim_create_user_command('TypstarAnkiReimport', anki.scan_reimport, {}) vim.api.nvim_create_user_command('TypstarAnkiForce', anki.scan_force, {}) + vim.api.nvim_create_user_command('TypstarAnkiForceReimport', anki.scan_force_reimport, {}) vim.api.nvim_create_user_command('TypstarAnkiForceCurrent', anki.scan_force_current, {}) + vim.api.nvim_create_user_command('TypstarAnkiForceCurrentReimport', anki.scan_force_current_reimport, {}) autosnippets.setup() end diff --git a/src/anki/anki_api.py b/src/anki/anki_api.py index b211d12..7844484 100644 --- a/src/anki/anki_api.py +++ b/src/anki/anki_api.py @@ -8,7 +8,7 @@ import aiohttp from .flashcard import Flashcard -async def _gather_exceptions(coroutines): +async def gather_exceptions(coroutines): for result in await asyncio.gather(*coroutines, return_exceptions=True): if isinstance(result, Exception): raise result @@ -28,7 +28,7 @@ class AnkiConnectApi: self.api_key = api_key self.semaphore = asyncio.Semaphore(2) # increase in case Anki implements multithreading - async def push_flashcards(self, cards: Iterable[Flashcard]): + async def push_flashcards(self, cards: Iterable[Flashcard], reimport: bool): add: dict[str, List[Flashcard]] = defaultdict(list) update: dict[str, List[Flashcard]] = defaultdict(list) n_add: int = 0 @@ -41,6 +41,14 @@ class AnkiConnectApi: else: update[card.deck].append(card) n_update += 1 + if reimport: + reimport_cards = await self._check_reimport(update) + print(f"Found {len(reimport_cards)} flashcards to reimport") + for card in reimport_cards: + update[card.deck].remove(card) + add[card.deck].append(card) + n_update -= 1 + n_add += 1 print( f"Pushing {n_add} new flashcards and {n_update} updated flashcards to Anki...", @@ -48,7 +56,7 @@ class AnkiConnectApi: ) await self._create_required_decks({*add.keys(), *update.keys()}) await self._add_new_cards(add) - await _gather_exceptions( + await gather_exceptions( [ *self._update_cards_requests(add), *self._update_cards_requests(update, True), @@ -115,7 +123,18 @@ class AnkiConnectApi: for deck in required: if deck not in existing: requests.append(self._request_api("createDeck", deck=deck)) - await _gather_exceptions(requests) + await gather_exceptions(requests) + + async def _check_reimport(self, cards_map: dict[str, List[Flashcard]]) -> List[Flashcard]: + cards = [] + for cs in cards_map.values(): + cards.extend(cs) + if not cards: + return [] + existing = await self._request_api( + "findNotes", query=f"nid:{','.join([str(c.note_id) for c in cards])}" + ) + return [c for c in cards if c.note_id not in existing] def _update_cards_requests( self, cards_map: dict[str, List[Flashcard]], update_deck: bool = True diff --git a/src/anki/flashcard.py b/src/anki/flashcard.py index ec8e23a..5ee2125 100644 --- a/src/anki/flashcard.py +++ b/src/anki/flashcard.py @@ -78,7 +78,7 @@ class Flashcard: self.back_node = back self.note_id_node = note_id - def update_id(self, value): + def update_id(self, value: int): if self.note_id != value: self.note_id = value self.id_updated = True diff --git a/src/anki/main.py b/src/anki/main.py index 648eb85..8296ae3 100644 --- a/src/anki/main.py +++ b/src/anki/main.py @@ -12,7 +12,9 @@ from anki.typst_compiler import TypstCompiler cli = typer.Typer(name="typstar-anki") -async def export_flashcards(root_dir, force_scan, clear_cache, typst_cmd, anki_url, anki_key): +async def export_flashcards( + root_dir, force_scan, clear_cache, reimport, typst_cmd, anki_url, anki_key +): parser = FlashcardParser() compiler = TypstCompiler(root_dir, typst_cmd) api = AnkiConnectApi(anki_url, anki_key) @@ -27,7 +29,7 @@ async def export_flashcards(root_dir, force_scan, clear_cache, typst_cmd, anki_u try: # async anki push - await api.push_flashcards(flashcards) + await api.push_flashcards(flashcards, reimport) finally: # write id updates to files parser.update_ids_in_source() @@ -57,13 +59,24 @@ def cmd( "as it clears hashes regardless of their path)" ), ] = False, + reimport: Annotated[ + bool, + typer.Option( + help="Instead of throwing an error also add flashcards that have already been asigned an id" + "but are not present in Anki. The asigned id will be updated." + ), + ] = False, typst_cmd: Annotated[ str, typer.Option(help="Typst command used for flashcard compilation") ] = "typst", anki_url: Annotated[str, typer.Option(help="Url for Anki-Connect")] = "http://127.0.0.1:8765", anki_key: Annotated[str | None, typer.Option(help="Api key for Anki-Connect")] = None, ): - asyncio.run(export_flashcards(root_dir, force_scan, clear_cache, typst_cmd, anki_url, anki_key)) + asyncio.run( + export_flashcards( + root_dir, force_scan, clear_cache, reimport, typst_cmd, anki_url, anki_key + ) + ) def main():