From 21834b169775bdff815940a07d6b69c59ab459f2 Mon Sep 17 00:00:00 2001 From: Alexander Alemayhu Date: Sun, 12 Apr 2026 17:10:44 +0200 Subject: [PATCH 1/3] fix: skip missing media files instead of crashing Filter out non-existent media files before passing to genanki, so users still get their flashcards even if images are missing from the upload. --- helpers/write_apkg.py | 6 +++++- tests/test_write_apkg.py | 33 ++++++++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/helpers/write_apkg.py b/helpers/write_apkg.py index 8c9fb3a..d483745 100644 --- a/helpers/write_apkg.py +++ b/helpers/write_apkg.py @@ -146,7 +146,11 @@ def _write_new_apkg(deck_payloads, media_files): """ decks, first_deck_id = create_decks(deck_payloads) package = Package(decks) - package.media_files = media_files + existing_media = [f for f in media_files if os.path.exists(f)] + if len(existing_media) < len(media_files): + missing = set(media_files) - set(existing_media) + print(f"Skipping {len(missing)} missing media file(s): {missing}", file=sys.stderr) + package.media_files = existing_media sanitized_name = sanitize_filename(deck_payloads[0]["name"]) if deck_payloads else "default" temp_path = write_package_to_temp_file(package) diff --git a/tests/test_write_apkg.py b/tests/test_write_apkg.py index c7915f7..c8d3074 100644 --- a/tests/test_write_apkg.py +++ b/tests/test_write_apkg.py @@ -96,12 +96,17 @@ def test_write_new_apkg_with_media(self, mock_replace, mock_package): "desc": "Test Description", "notes": [note] } - media_files = ['test.jpg', 'audio.mp3'] - + with tempfile.TemporaryDirectory() as tmpdir: + img_path = os.path.join(tmpdir, 'test.jpg') + audio_path = os.path.join(tmpdir, 'audio.mp3') + open(img_path, 'w').close() + open(audio_path, 'w').close() + media_files = [img_path, audio_path] + with mock.patch('os.getcwd', return_value=tmpdir): _write_new_apkg([deck_payload], media_files) - + mock_package.assert_called_once() package_instance = mock_package.return_value self.assertEqual(package_instance.media_files, media_files) @@ -110,6 +115,28 @@ def test_write_new_apkg_with_media(self, mock_replace, mock_package): mock_package.return_value.write_to_file.assert_called_once() mock_replace.assert_called_once() + @mock.patch('helpers.write_apkg.Package') + @mock.patch('helpers.write_apkg.os.replace') + def test_write_new_apkg_skips_missing_media(self, mock_replace, mock_package): + note = Note(model=self.test_model, fields=['Q1', 'A1']) + deck_payload = { + "id": 1234567890, + "name": "Test Deck", + "desc": "Test Description", + "notes": [note] + } + + with tempfile.TemporaryDirectory() as tmpdir: + existing_path = os.path.join(tmpdir, 'exists.jpg') + open(existing_path, 'w').close() + media_files = [existing_path, 'image.png'] + + with mock.patch('os.getcwd', return_value=tmpdir): + _write_new_apkg([deck_payload], media_files) + + package_instance = mock_package.return_value + self.assertEqual(package_instance.media_files, [existing_path]) + @mock.patch('helpers.write_apkg.Package') @mock.patch('helpers.write_apkg.os.replace') def test_write_new_apkg_empty_deck_list(self, mock_replace, mock_package): From c6972d30d50d49f82f086e7e7fc71abc7809d563 Mon Sep 17 00:00:00 2001 From: Alexander Alemayhu Date: Mon, 4 May 2026 21:53:33 +0200 Subject: [PATCH 2/3] fix: raise ValueError instead of silent sys.exit(1) for empty deck data sys.exit(1) raises SystemExit which is not caught by the except Exception block, so failures from empty pages produced no output and no error email. Replacing with ValueError gives the caller a useful message in stderr and triggers the production error email flow. Co-Authored-By: Claude Sonnet 4.6 --- create_deck.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/create_deck.py b/create_deck.py index 3c9e5d4..42e9522 100644 --- a/create_deck.py +++ b/create_deck.py @@ -38,7 +38,7 @@ decks = [] if len(data) == 0: - sys.exit(1) + raise ValueError("No cards were generated. The page may be empty or contain no supported toggle lists.") # Model / Template stuff mt = data[0].get("settings", {}) From 8dd1442714b701b8361e51c38cc8b15cffd1025d Mon Sep 17 00:00:00 2001 From: Alexander Alemayhu Date: Mon, 4 May 2026 21:58:45 +0200 Subject: [PATCH 3/3] fix: redirect all diagnostic prints to stderr to keep stdout clean MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stdout is the machine-readable channel — it carries only the final .apkg file path. Warnings and error messages going to stdout could corrupt that output and cause the Node.js caller to resolve with a warning string instead of a valid path. Co-Authored-By: Claude Sonnet 4.6 --- helpers/cards.py | 5 +++-- helpers/write_apkg.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/helpers/cards.py b/helpers/cards.py index 248ee3e..58300c0 100644 --- a/helpers/cards.py +++ b/helpers/cards.py @@ -1,6 +1,7 @@ """ Helper functions for working with the flashcards """ +import sys import ftfy @@ -14,12 +15,12 @@ def get_safe_value(value): try: value = value.decode('utf-8') # Decode bytes to string, handling potential errors except UnicodeDecodeError: - print("Warning: Could not decode bytes using utf-8. Returning empty string.") + print("Warning: Could not decode bytes using utf-8. Returning empty string.", file=sys.stderr) return "" elif value is None: return "" elif not isinstance(value, str): - print(f"Warning: get_safe_value received unexpected input type: {type(value)}. Returning empty string.") + print(f"Warning: get_safe_value received unexpected input type: {type(value)}. Returning empty string.", file=sys.stderr) return "" return ftfy.fixes.fix_surrogates(value) diff --git a/helpers/write_apkg.py b/helpers/write_apkg.py index d483745..e85dc1d 100644 --- a/helpers/write_apkg.py +++ b/helpers/write_apkg.py @@ -69,7 +69,7 @@ def write_package_to_temp_file(package): try: package.write_to_file(temp_path) except Exception as e: - print(f"Error writing to temporary file: {e}") + print(f"Error writing to temporary file: {e}", file=sys.stderr) raise return temp_path