mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-11-15 18:28:13 +00:00
Six places read the Supported Book Formats and Supported Audiobook Formats settings on their own: post-processing, the post-process scan, IRC, slskd, Prowlarr and direct download. For ebooks they already agreed, since they all fall back to `DEFAULT_EBOOK_FORMATS`. For audiobooks they did not. The settings registry and slskd fall back to every audio format plus zip and rar, while post-processing and IRC fell back to m4b and mp3 only. They now share `shelfmark/core/formats.py`, with `get_supported_formats`, `get_supported_audiobook_formats` and a content-type aware `get_supported_formats_for`, and a shared `DEFAULT_AUDIOBOOK_FORMATS` in `core/utils.py` next to the ebook one. `policy.py` re-exports the same functions, so nothing that imports them from there changes. It is not in `policy.py` itself because importing the `postprocess` package pulls in the router, which imports the release sources, so a source could not import from it without a cycle. One behaviour difference, and only in the fallback: with the audiobook setting unset, or emptied, post-processing and IRC used to accept m4b and mp3 and now accept the full default list like slskd and the settings page already did. A configured list is read exactly as before. Direct download's own helper used the raw configured list and now reads the same normalized one, which is identical for any list the settings page can save. Also public now: `parse_size` in the Prowlarr source (slskd and Newznab imported it with the underscore) and `coerce_completed_path_timeout_seconds` in the client base handler (slskd imported that too). Not changed on purpose: slskd's `_coerce_int` looks like a copy of `coerce_int` but is not. It reads `"12.0"` as 12 and `True` as 0, where `coerce_int` gives 0 and 1. And the `_parse_size` in the IRC source is a different function, string to bytes. Tests: a new set checks that post-processing, IRC and slskd all fall back to the shared defaults and that the settings default is the shared one, and the scan-archive and IRC tests now patch the shared config instead of the per-module copies. Full suite passes.
104 lines
3.3 KiB
Python
104 lines
3.3 KiB
Python
from __future__ import annotations
|
|
|
|
import zipfile
|
|
|
|
from shelfmark.core import formats as formats_mod
|
|
from shelfmark.core.models import DownloadTask
|
|
from shelfmark.core.utils import DEFAULT_AUDIOBOOK_FORMATS
|
|
from shelfmark.download import archive as archive_mod
|
|
from shelfmark.download.postprocess import scan as scan_mod
|
|
|
|
# The shipped default for SUPPORTED_AUDIOBOOK_FORMATS, which includes archives.
|
|
_DEFAULT_AUDIOBOOK_FORMATS = list(DEFAULT_AUDIOBOOK_FORMATS)
|
|
|
|
|
|
def _audiobook_task() -> DownloadTask:
|
|
return DownloadTask(task_id="t", source="audiobookbay", title="Test", content_type="audiobook")
|
|
|
|
|
|
def _use_default_audiobook_formats(monkeypatch, tmp_path) -> None:
|
|
monkeypatch.setattr(
|
|
formats_mod, "get_supported_audiobook_formats", lambda: list(_DEFAULT_AUDIOBOOK_FORMATS)
|
|
)
|
|
monkeypatch.setattr(
|
|
archive_mod, "get_supported_audiobook_formats", lambda: list(_DEFAULT_AUDIOBOOK_FORMATS)
|
|
)
|
|
staging = tmp_path / "staging"
|
|
|
|
def build_staging_dir(prefix, _task_id):
|
|
path = staging / str(prefix)
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
return path
|
|
|
|
monkeypatch.setattr(scan_mod, "build_staging_dir", build_staging_dir)
|
|
|
|
|
|
def _write_series_zip(directory):
|
|
directory.mkdir()
|
|
archive = directory / "Series - Books 1-3.zip"
|
|
with zipfile.ZipFile(archive, "w") as zf:
|
|
for n in (1, 2, 3):
|
|
zf.writestr(f"Series/Book {n}/Book {n}.mp3", b"audio")
|
|
return archive
|
|
|
|
|
|
def test_archive_is_not_counted_as_book_file(tmp_path, monkeypatch) -> None:
|
|
_use_default_audiobook_formats(monkeypatch, tmp_path)
|
|
download = tmp_path / "download"
|
|
archive = _write_series_zip(download)
|
|
|
|
book_files, _rejected, archive_files, error = scan_mod.scan_directory_tree(
|
|
download, "audiobook"
|
|
)
|
|
|
|
assert error is None
|
|
assert book_files == []
|
|
assert archive_files == [archive]
|
|
|
|
|
|
def test_archive_is_extracted_with_default_audiobook_formats(tmp_path, monkeypatch) -> None:
|
|
_use_default_audiobook_formats(monkeypatch, tmp_path)
|
|
download = tmp_path / "download"
|
|
_write_series_zip(download)
|
|
|
|
files, _rejected, _cleanup, error = scan_mod.collect_directory_files(
|
|
download,
|
|
_audiobook_task(),
|
|
allow_archive_extraction=True,
|
|
)
|
|
|
|
assert error is None
|
|
assert sorted(f.name for f in files) == ["Book 1.mp3", "Book 2.mp3", "Book 3.mp3"]
|
|
|
|
|
|
def test_archive_imported_as_is_when_extraction_disabled(tmp_path, monkeypatch) -> None:
|
|
_use_default_audiobook_formats(monkeypatch, tmp_path)
|
|
download = tmp_path / "download"
|
|
archive = _write_series_zip(download)
|
|
|
|
files, _rejected, _cleanup, error = scan_mod.collect_directory_files(
|
|
download,
|
|
_audiobook_task(),
|
|
allow_archive_extraction=False,
|
|
)
|
|
|
|
assert error is None
|
|
assert files == [archive]
|
|
|
|
|
|
def test_loose_audio_still_preferred_over_archives(tmp_path, monkeypatch) -> None:
|
|
_use_default_audiobook_formats(monkeypatch, tmp_path)
|
|
download = tmp_path / "download"
|
|
_write_series_zip(download)
|
|
loose = download / "Book.m4b"
|
|
loose.write_bytes(b"audio")
|
|
|
|
files, _rejected, _cleanup, error = scan_mod.collect_directory_files(
|
|
download,
|
|
_audiobook_task(),
|
|
allow_archive_extraction=True,
|
|
)
|
|
|
|
assert error is None
|
|
assert files == [loose]
|