From 2a06d0a48845bd9cd6a0e0deb53f03c8fa981204 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Sun, 4 Oct 2026 18:38:42 +0200 Subject: [PATCH] fix(export): ship untagged file when provenance tagging fails Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 2 ++ Manual.md | 2 +- routes/magazine.py | 3 ++- tests/test_epub_export.py | 7 +++++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea56a4a..88e1649 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ยท versioned wi - **Voice list rows no longer show the "Source unknown" badge** (it crowded the play buttons). The badge stays in the voice inspector, next to the Source & consent panel. ### Fixed +- **Voice archetype gallery broke `npm run check:frontend`**: its `say` helper collided with `magazine.js`; renamed to `archSay`. +- **Export failed with HTTP 500 when provenance tagging failed** (EPUB/ZIP/article MP3 with audio ffmpeg cannot tag); it now ships the untagged file and logs a warning. - **Archetype `language` filter accepted only full names.** `GET /api/archetypes?language=de` returned 0 items; it now accepts ISO codes and names, case-insensitive (`de`, `DE`, `German`); unknown values return an empty list. - **LibriVox search returned 502 "unavailable" after 20-30 s.** Cause: the LibriVox API is erratic (10-40 s for any request, measured from host and container; egress/DNS/TLS fine) and the old call asked for `extended=1` (all chapters of 15 books) with a 30 s timeout, mapping every failure to one generic message. Now the list is light (no `extended`), the first chapter is fetched per book on demand (`GET /api/voice-sources/librivox/book/{id}`, UI "Load preview" / on import), timeout 25 s with one retry and backoff, failures cached 30 s, distinct messages (timeout / unreachable / HTTP status / unreadable) and a privacy-safe log line (exception class and query length only). Host/redirect rules unchanged. - **`export_provenance_default` missing from settings defaults.** It is now in the defaults (`true`), so `GET /api/settings` exposes it. diff --git a/Manual.md b/Manual.md index f7067e7..553c138 100644 --- a/Manual.md +++ b/Manual.md @@ -796,7 +796,7 @@ sent. Websites that redistribute commercial magazines without a licence are deli - **Prerequisites:** an article with generated audio. No extra software. - **Steps:** in the export row tick or untick "Mark as synthetic speech (tags)" (default on, remembered as a setting) and optionally "include voice name". Then download as usual. - **Result:** the copy you download has tags `synthetic_speech`, `generator`, `generator_version`, `generated_date` (and `voice` if chosen). The stored audio is unchanged. To check a file: `python -m core.export_provenance file.mp3` or `POST /api/export/provenance` with the file as body. -- **Errors:** if tagging fails the download answers "Could not add provenance tags"; untick the box to export untagged. +- **Errors:** if tagging fails (e.g. ffmpeg cannot read the file) the download still succeeds with the untagged file and a server-log warning; untick the box to export untagged on purpose. - **Mobile / accessibility:** plain labelled checkboxes. **Offline:** works offline. **Privacy:** no user names or paths; the voice name only if you tick it. Tags can be removed by anyone, so they are an honest label, not protection. An inaudible watermark and a spoken disclosure are not available yet. ### Errors diff --git a/routes/magazine.py b/routes/magazine.py index 2d2dff4..c2d7add 100644 --- a/routes/magazine.py +++ b/routes/magazine.py @@ -3311,7 +3311,8 @@ def _export_mp3(mp3: Path, tmpdir: Path, provenance: str, voice: str) -> Path: try: return ep.apply_tags(mp3, tmpdir / mp3.name, ep.build_tags(__version__, name, voice == "1")) except (RuntimeError, OSError, __import__("subprocess").SubprocessError): - raise HTTPException(500, "Could not add provenance tags to the export") + logging.getLogger(__name__).warning("provenance tagging failed for %s; shipping the untagged file", mp3.name) + return mp3 # degrade gracefully: an untagged export beats a failed download @router.post("/api/export/provenance") diff --git a/tests/test_epub_export.py b/tests/test_epub_export.py index 255f45d..5848140 100644 --- a/tests/test_epub_export.py +++ b/tests/test_epub_export.py @@ -279,6 +279,13 @@ class EndpointTests(unittest.TestCase): self.assertEqual(z.namelist()[0], "mimetype") self.assertIn("exact", z.read("OEBPS/content.opf").decode()) + def test_provenance_tag_failure_ships_untagged_file(self): + from unittest import mock + self.seed() + with mock.patch("core.export_provenance.apply_tags", side_effect=RuntimeError("tagging failed")): + r = self.client.get(f"/api/magazine/{self.doc}/epub?provenance=1") + self.assertEqual(r.status_code, 200, r.text[:200]) + def test_unapproved_or_silent_articles_are_not_packaged(self): self.seed(approved=False) self.assertEqual(self.client.get(f"/api/magazine/{self.doc}/epub").status_code, 404)