mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-09-24 13:30:47 +01:00
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:
co-authored by
Claude Fable 5
parent
ddc26f01b6
commit
65e2e3be20
@@ -247,7 +247,7 @@ Seconds since the last WireGuard handshake before the healthcheck bounces the tu
|
||||
| `CALIBRE_WEB_URL` | Adds a navigation button to your book library (Calibre-Web Automated, Grimmory, etc). | string | _none_ |
|
||||
| `AUDIOBOOK_LIBRARY_URL` | Adds a separate navigation button for your audiobook library (Audiobookshelf, Plex, etc). When both URLs are set, icons are shown instead of text. | string | _none_ |
|
||||
| `SUPPORTED_FORMATS` | Book formats to include in search results. ZIP/RAR archives are extracted automatically and book files are used if found. | string (comma-separated) | `epub,mobi,azw3,fb2,djvu,cbz,cbr` |
|
||||
| `SUPPORTED_AUDIOBOOK_FORMATS` | Audiobook formats to include in search results. ZIP/RAR archives are extracted automatically and audiobook files are used if found. | string (comma-separated) | `m4b,mp3,m4a,flac,ogg,wma,aac,wav,opus,zip,rar` |
|
||||
| `SUPPORTED_AUDIOBOOK_FORMATS` | Audiobook formats to include in search results. ZIP/RAR archives are extracted automatically and audiobook files are used if found. | string (comma-separated) | `m4b,mp3,m4a,mp4,flac,ogg,wma,aac,wav,opus,zip,rar` |
|
||||
| `BOOK_LANGUAGE` | Default language filter for searches. | string (comma-separated) | `en` |
|
||||
|
||||
<details>
|
||||
@@ -296,7 +296,7 @@ Book formats to include in search results. ZIP/RAR archives are extracted automa
|
||||
Audiobook formats to include in search results. ZIP/RAR archives are extracted automatically and audiobook files are used if found.
|
||||
|
||||
- **Type:** string (comma-separated)
|
||||
- **Default:** `m4b,mp3,m4a,flac,ogg,wma,aac,wav,opus,zip,rar`
|
||||
- **Default:** `m4b,mp3,m4a,mp4,flac,ogg,wma,aac,wav,opus,zip,rar`
|
||||
|
||||
#### `BOOK_LANGUAGE`
|
||||
|
||||
|
||||
@@ -122,7 +122,12 @@ def is_audiobook(content_type: str | None) -> bool:
|
||||
# had drifted apart: the settings UI only offered m4b/mp3/m4a, which meant a FLAC
|
||||
# audiobook could never be enabled, was silently dropped from every search result, and
|
||||
# was rejected after download as "format not supported".
|
||||
AUDIOBOOK_FORMATS = ("m4b", "mp3", "m4a", "flac", "ogg", "wma", "aac", "wav", "opus")
|
||||
#
|
||||
# "mp4" is here because some trackers (MyAnonamouse in particular) ship AAC audiobooks
|
||||
# as per-chapter .mp4 files - the same ISO-BMFF container as .m4a/.m4b, just with the
|
||||
# generic extension. Without it those releases downloaded fine and then failed
|
||||
# post-processing with "No book files found in download".
|
||||
AUDIOBOOK_FORMATS = ("m4b", "mp3", "m4a", "mp4", "flac", "ogg", "wma", "aac", "wav", "opus")
|
||||
|
||||
# Multi-file audiobooks are almost always distributed as an archive. These are containers
|
||||
# rather than formats: they are what a *release* looks like, and the formats above are
|
||||
|
||||
@@ -76,6 +76,7 @@ _BOOK_EXTENSIONS = (
|
||||
".m4b",
|
||||
".mobi",
|
||||
".mp3",
|
||||
".mp4",
|
||||
".ogg",
|
||||
".opus",
|
||||
".pdf",
|
||||
|
||||
@@ -80,6 +80,7 @@ _BOOK_EXTENSIONS = (
|
||||
".m4b",
|
||||
".mobi",
|
||||
".mp3",
|
||||
".mp4",
|
||||
".ogg",
|
||||
".opus",
|
||||
".pdf",
|
||||
|
||||
@@ -428,14 +428,15 @@ class IRCReleaseSource(ReleaseSource):
|
||||
"m4b": 0,
|
||||
"mp3": 1,
|
||||
"m4a": 2,
|
||||
"flac": 3,
|
||||
"opus": 4,
|
||||
"ogg": 5,
|
||||
"aac": 6,
|
||||
"wav": 7,
|
||||
"wma": 8,
|
||||
"rar": 9,
|
||||
"zip": 10,
|
||||
"mp4": 3,
|
||||
"flac": 4,
|
||||
"opus": 5,
|
||||
"ogg": 6,
|
||||
"aac": 7,
|
||||
"wav": 8,
|
||||
"wma": 9,
|
||||
"rar": 10,
|
||||
"zip": 11,
|
||||
}
|
||||
|
||||
def _convert_to_releases(
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user