mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-09-24 22:05:20 +01:00
An IRC audiobook search returned nothing while OpenBooks, reading the same @search answer from the same channel, listed results. Three separate defects were discarding them. The audiobook format list was maintained by hand in four places and had drifted. The settings UI offered only m4b/mp3/m4a/zip/rar, and that list is the only one a user's config can be built from, so flac, opus, ogg, aac, wav and wma were unreachable everywhere — even though the IRC parser recognized them, the IRC sorter ranked them (dead code that could never fire), archive extraction knew them and Prowlarr searched for them. A FLAC audiobook was invisible in search and, if it arrived anyway, rejected after download as "format not supported". AUDIOBOOK_FORMATS and ARCHIVE_FORMATS now live once in core.utils and every layer derives from them, which also restored the missing .opus in the post-download scan's trackable extensions. Widening the default alone would not have reached anyone already affected: initialize_default_configs() writes field defaults only when a tab has no config file yet, so an existing install keeps its persisted m4b/mp3 list forever. migrate_audiobook_formats rewrites a list that still matches the old default exactly and leaves every other value alone — re-enabling formats someone had deliberately turned off would be worse than leaving them narrow. The IRC parser filtered by file extension alone. Multi-file audiobooks ship as a .rar or .zip of MP3s, which matched neither SUPPORTED_FORMATS nor SUPPORTED_AUDIOBOOK_FORMATS, so they fell out of the ebook bucket and the audiobook bucket both. Results are now classified before the format filter is applied: an audio extension means audiobook, an ebook extension means ebook, and for a container — where the extension says nothing about the contents — the release name decides. An ebook archive stays out of audiobook results. RESULT_LINE_REGEX matched \w+ after any dot, so a line carrying no file extension parsed as format "5mb" out of "::INFO:: 620.5MB", taking the title and the size down with it and guaranteeing every downstream filter dropped it. Any decimal size did this. The extension is now matched against the known formats, so such a line falls through to the simple pattern and comes back as "unknown", which the rest of the parser already handles. ALL_RECOGNIZED_FORMATS became an ordered tuple in the process: it was a set, so which extension won for a line naming two of them depended on set iteration order and could vary between restarts. Refs #1129
134 lines
5.1 KiB
Python
134 lines
5.1 KiB
Python
import pytest
|
|
|
|
from shelfmark.core.utils import ARCHIVE_FORMATS, AUDIOBOOK_FORMATS
|
|
from shelfmark.release_sources.irc import parser
|
|
|
|
# What a stock install actually filters with, so these tests fail if the defaults regress.
|
|
_DEFAULT_CONFIG = {
|
|
"SUPPORTED_FORMATS": ["epub", "mobi", "azw3", "fb2", "djvu", "cbz", "cbr"],
|
|
"SUPPORTED_AUDIOBOOK_FORMATS": [*AUDIOBOOK_FORMATS, *ARCHIVE_FORMATS],
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def default_formats(monkeypatch):
|
|
"""Filter with the shipped default format lists."""
|
|
monkeypatch.setattr(
|
|
parser.config, "get", lambda key, default=None: _DEFAULT_CONFIG.get(key, default)
|
|
)
|
|
|
|
|
|
def test_audiobook_archives_are_found_with_default_settings(default_formats):
|
|
"""Regression for #1129: audiobooks ship as .rar/.zip and were dropped by both buckets.
|
|
|
|
A single "@search" answers with one file holding every format. These are the lines an
|
|
audiobook actually occupies in it - the extension is a container, and the release name
|
|
is the only thing saying what is inside.
|
|
"""
|
|
content = "\n".join(
|
|
[
|
|
"!Oatmeal Andy Weir - Project Hail Mary (Audiobook) [MP3 64kbps].rar ::INFO:: 620.5MB",
|
|
"!DV8 Andy Weir - Project Hail Mary - Audiobook.zip ::INFO:: 700MB",
|
|
"!Horla Andy Weir - Project Hail Mary [Unabridged].m4b ::INFO:: 850.1MB",
|
|
]
|
|
)
|
|
|
|
results = parser.parse_results_file(content, content_type="audiobook")
|
|
|
|
assert [result.format for result in results] == ["rar", "zip", "m4b"]
|
|
|
|
|
|
def test_ebook_archive_does_not_leak_into_audiobook_results(default_formats):
|
|
"""An ebook .rar must not be offered as an audiobook just because it is an archive."""
|
|
content = "!bald Andy Weir - Project Hail Mary (retail).rar ::INFO:: 2.1MB"
|
|
|
|
assert parser.parse_results_file(content, content_type="audiobook") == []
|
|
|
|
|
|
def test_flac_audiobook_is_reachable_with_default_settings(default_formats):
|
|
"""FLAC was recognized by the parser and ranked by the sorter, but never selectable."""
|
|
content = "!Ook Andy Weir - Project Hail Mary.flac ::INFO:: 1.1GB"
|
|
|
|
results = parser.parse_results_file(content, content_type="audiobook")
|
|
|
|
assert [result.format for result in results] == ["flac"]
|
|
|
|
|
|
def test_decimal_size_is_not_mistaken_for_a_file_extension():
|
|
"""A line with no extension used to parse as format="5mb" out of "::INFO:: 620.5MB"."""
|
|
line = "!Ook Andy Weir - Project Hail Mary (2021) Audiobook ::INFO:: 620.5MB"
|
|
|
|
result = parser.parse_result_line(line)
|
|
|
|
assert result.format == "unknown"
|
|
assert result.title == "Project Hail Mary (2021) Audiobook"
|
|
assert result.size == "620.5MB"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("line", "expected"),
|
|
[
|
|
("!s A - T.epub ::INFO:: 1MB", "ebook"),
|
|
("!s A - T.mp3 ::INFO:: 1MB", "audiobook"),
|
|
("!s A - T.flac ::INFO:: 1MB", "audiobook"),
|
|
# Archives carry no format information, so the name has to decide.
|
|
("!s A - T (Audiobook).rar ::INFO:: 1MB", "audiobook"),
|
|
("!s A - T [Unabridged].zip ::INFO:: 1MB", "audiobook"),
|
|
("!s A - T (Narrated by Someone).rar ::INFO:: 1MB", "audiobook"),
|
|
("!s A - T [64kbps].zip ::INFO:: 1MB", "audiobook"),
|
|
("!s A - T (retail).rar ::INFO:: 1MB", "ebook"),
|
|
("!s A - T.zip ::INFO:: 1MB", "ebook"),
|
|
],
|
|
)
|
|
def test_detect_content_type(line, expected):
|
|
assert parser.detect_content_type(parser.parse_result_line(line)) == expected
|
|
|
|
|
|
def test_recognized_formats_order_is_deterministic():
|
|
"""This was a set, so which format won for a multi-extension line varied per restart."""
|
|
assert parser.ALL_RECOGNIZED_FORMATS == tuple(parser.ALL_RECOGNIZED_FORMATS)
|
|
# Longest-first, so ".azw3" cannot be truncated to "azw" (nor ".docx" to "doc").
|
|
lengths = [len(fmt) for fmt in parser.ALL_RECOGNIZED_FORMATS]
|
|
assert lengths == sorted(lengths, reverse=True)
|
|
|
|
|
|
def test_parse_results_file_uses_audiobook_format_settings(monkeypatch):
|
|
values = {
|
|
"SUPPORTED_FORMATS": ["epub"],
|
|
"SUPPORTED_AUDIOBOOK_FORMATS": ["zip", "mp3"],
|
|
}
|
|
|
|
monkeypatch.setattr(parser.config, "get", lambda key, default=None: values.get(key, default))
|
|
|
|
content = "\n".join(
|
|
[
|
|
"!AudioBot Author Name - Great Audio Book.zip ::INFO:: 1.2GB",
|
|
"!AudioBot Author Name - Great Audio Book.mp3 ::INFO:: 1.1GB",
|
|
"!AudioBot Author Name - Great Audio Book.epub ::INFO:: 5MB",
|
|
]
|
|
)
|
|
|
|
results = parser.parse_results_file(content, content_type="audiobook")
|
|
|
|
assert [result.format for result in results] == ["zip", "mp3"]
|
|
|
|
|
|
def test_parse_results_file_uses_book_format_settings_for_ebooks(monkeypatch):
|
|
values = {
|
|
"SUPPORTED_FORMATS": ["epub"],
|
|
"SUPPORTED_AUDIOBOOK_FORMATS": ["zip", "mp3"],
|
|
}
|
|
|
|
monkeypatch.setattr(parser.config, "get", lambda key, default=None: values.get(key, default))
|
|
|
|
content = "\n".join(
|
|
[
|
|
"!BookBot Author Name - Great Book.zip ::INFO:: 50MB",
|
|
"!BookBot Author Name - Great Book.epub ::INFO:: 5MB",
|
|
]
|
|
)
|
|
|
|
results = parser.parse_results_file(content, content_type="ebook")
|
|
|
|
assert [result.format for result in results] == ["epub"]
|