feat(audiobook): recognise .mp4 as an audiobook format (#1264)

## Problem

Some trackers — MyAnonamouse in particular — distribute AAC audiobooks
as per-chapter `.mp4` files. That's the same ISO-BMFF container as
`.m4a`/`.m4b`, just with the generic extension (`ftyp isom`,
audio-only).

Today those releases:
1. show up in Prowlarr search results with **no format chip** — only the
generic "Audiobook" icon, because no format could be inferred;
2. download successfully; then
3. fail post-processing with **"No book files found in download"**,
because `.mp4` isn't in `AUDIOBOOK_FORMATS` (`shelfmark/core/utils.py`).

Real example: MAM #627978, *The Martian* (Andy Weir, 2020 edition) — 142
files `0001 … 0142 Andy Weir (2020) The Martian.mp4` + `cover.jpg`, 305
MB. Every file is a valid AAC-in-MP4 chapter.

Adding `mp4` to `SUPPORTED_AUDIOBOOK_FORMATS` in `settings.json` doesn't
help since the hard-coded tuple is what post-processing scans against.

## Change

- Add `"mp4"` to `AUDIOBOOK_FORMATS` (single source of truth — settings
UI, Prowlarr parsing, IRC parser, archive extraction and post-download
scan all derive from it), with a comment explaining why.
- Add `".mp4"` to the two hand-maintained debrid `_BOOK_EXTENSIONS`
lists (AllDebrid / Real-Debrid) so file selection matches.
- Slot `mp4` into the IRC `AUDIOBOOK_FORMAT_PRIORITY` table right after
`m4a` (same container family).
- Update the documented default in `docs/environment-variables.md`.
- New regression test
`test_audiobook_multifile_mp4_chapters_are_book_files` modelled on the
existing multi-file usenet test.

### Note for existing installs

The legacy-default migration only widens configs that still hold the old
`m4b,mp3` list, so users on the current widened default won't pick up
`mp4` automatically — they'll need to tick it in Settings → Audiobook
formats. New installs get it by default. Happy to extend the migration
if you'd rather it be automatic.

## Testing

- `ruff check` / `ruff format --check`: clean
- `pytest tests/core tests/config tests/irc tests/prowlarr
tests/download -m "not integration and not e2e"`: 2296 passed, new test
+ `test_audiobook_format_consistency.py` all green. The 10 failures in
`test_entrypoint_permissions.py` / `test_orchestrator_stall.py`
reproduce identically on untouched `main` on macOS and are unrelated.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jakesterpdx
2026-08-24 17:45:54 -04:00
committed by GitHub
co-authored by Claude Fable 5
parent ddc26f01b6
commit 65e2e3be20
6 changed files with 63 additions and 11 deletions
+44
View File
@@ -1458,3 +1458,47 @@ def test_external_directory_prefers_files_over_archives_and_keeps_source(
# TMP staging should be cleaned.
assert list(staging.iterdir()) == []
def test_audiobook_multifile_mp4_chapters_are_book_files(tmp_path):
"""Per-chapter .mp4 audiobooks (as MyAnonamouse ships AAC releases) must be
recognised as book files instead of failing with "No book files found"."""
from shelfmark.download.postprocess.router import (
post_process_download as _post_process_download,
)
source_dir = tmp_path / "downloads" / "Andy Weir (2020) The Martian"
source_dir.mkdir(parents=True)
for part in (1, 2):
(source_dir / f"{part:04d} Andy Weir (2020) The Martian.mp4").write_text(f"audio{part}")
(source_dir / "cover.jpg").write_text("jpg")
ingest = tmp_path / "ingest"
ingest.mkdir()
task = DownloadTask(
task_id="mp4-audio-grouped",
source="prowlarr",
title="The Martian",
author="Andy Weir",
format="mp4",
content_type="audiobook",
search_mode=SearchMode.UNIVERSAL,
)
with patch("shelfmark.core.config.config") as mock_config:
mock_config.get = _build_config(
ingest,
organization="rename_and_group",
supported_audiobook_formats=["mp4"],
)
mock_config.CUSTOM_SCRIPT = None
result = _post_process_download(source_dir, task, Event(), lambda *_args: None)
grouped_dir = ingest / "Andy Weir (2020) The Martian"
assert result is not None
assert Path(result).parent == grouped_dir
assert sorted(path.name for path in grouped_dir.glob("*.mp4")) == [
"0001 Andy Weir (2020) The Martian.mp4",
"0002 Andy Weir (2020) The Martian.mp4",
]