mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-11-15 08:48:26 +00:00
fix(auto-download): match Usenet ebooks by the format in their name (#1704)
Automatic downloads reject Usenet ebook releases whose format is written as `(epub)` or `[EPUB]` rather than `.epub`. A Hardcover ebook request whose match is something like `Rick Rubin - The Creative Act (retail) (epub)`, from Newznab or from a Prowlarr indexer other than MyAnonamouse, stays pending with `no strict match`. For an ebook request, `_format_match` wants a detected format, content type `ebook`, or a supported format after a dot in the title, and these releases have none of the three. `_prowlarr_result_to_release` only detects a format for MyAnonamouse, `_newznab_result_to_release` never sets one, `_detect_content_type_from_categories` maps every 7000–7999 category to `book`, and the names put the format in brackets. The rule came in with ebook auto-download in #1533. The title check now looks for a supported format among the title's words, using the `_tokens` helper that title and author matching already use. `Dune.epub`, `(epub)`, `[EPUB]`, `{MOBI}` and `- azw3` all count, and a word that only contains a format, like "Mobile" or "Republished", doesn't. It replaces the `.<format>` check, which it covers. The audiobook check is unchanged, and `docs/hardcover-sync.md` now describes the rule as it works. I didn't make content type `book` count instead. Magazines, comics and results with no category get it too, so it would drop the format requirement for nearly every Newznab and Prowlarr release. One side effect: with the default formats, a bracketed `(cbr)` or `(cbz)` now counts where before only `.cbr` and `.cbz` did. New tests in `TestContentTypeAwareMatching` use a release shaped like the Usenet ones (no format, content type `book`, NZB). The Creative Act title matches, the bracketed and dashed forms count, `(m4b)`, `(epub) (Unabridged)` and a title with no format still fail, and Mobile, Republished and epubs don't count. The five positive cases fail on main. `make python-checks` and the auto-download tests pass, and the full suite has no new failures against main. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Bug Fixes** * Ebook releases can now match supported formats such as EPUB, AZW3, and MOBI when the format appears as a whole word in the release title, including in parentheses or brackets. * Titles containing a format name only as part of a longer word, such as “Mobile,” won’t be mistaken for that format. * **Documentation** * Clarified how format matching works for sources that don’t report an ebook format. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
44e1c6a660
commit
2fbe1dedc2
3 files changed
+68
-3
No files matched your search
@@ -30,7 +30,7 @@ A release is a strict match only when all of the following hold:
|
||||
- the release title doesn't name a different book that contains the requested one. A "Dune" request won't take *Dune Messiah* or *Children of Dune*, and a bare number after the title (*Mistborn 2*, *Mistborn Two*, *Dune Chronicles Book II*) names a sequel. A title with a number of its own (*Ready Player One*) takes no other number after it, 1 included. Only the parts of the release name that hold the title are judged, so narrators, uploader tags and the book's own series, author and subtitle words don't count against it; a part that is just the series name (*Alex Cross 25*) does not vouch for a one-word title, so a "Cross" request won't take *Alex Cross: Cross Justice*. Two slash-separated names that both hold the title (*Dune / Dune Messiah*) are two books;
|
||||
- the release is not a derivative of the work: *summary*, *analysis*, *study guide*, *workbook*, *abridged*, *dramatized*, *adaptation* or *graphic novel* anywhere in the name disqualifies it, unless the requested title itself carries the word. *Unabridged* is a full recording and is fine;
|
||||
- the release is not a multi-book pack: ranges like `1-24` or `Books 1 to 17`, or words like *complete*, *collection*, *box set*, *omnibus*, *trilogy*, *megapack*, *+ short stories*, unless the requested title itself carries them. "Complete and Unabridged" describes one recording and doesn't count. The library check uses the same word list;
|
||||
- the format fits the request — an audiobook request needs an audiobook signal (`m4b`/`mp3`, a release from an audiobook-only source, or "audiobook"/"unabridged" in the title) and is never an ebook-only file; an ebook request needs one of your **Supported Book Formats** (or an `.epub`-style marker in the title) and no audiobook signal;
|
||||
- the format fits the request — an audiobook request needs an audiobook signal (`m4b`/`mp3`, a release from an audiobook-only source, or "audiobook"/"unabridged" in the title) and is never an ebook-only file; an ebook request needs one of your **Supported Book Formats**, either reported by the source or named as a whole word in the release title (`Dune.epub`, `(epub)`, `[EPUB]`; *Mobile* is not `mobi`), and no audiobook signal. Usenet and most Prowlarr indexers report no format, so for them the title is what counts;
|
||||
- for torrents that report a seeder count, it meets **Minimum seeders** — sources that report none (AudiobookBay) are not judged on seeders.
|
||||
|
||||
AudiobookBay requires every word of a query, and its posts for one volume of a multi-part book often spell the part differently from the request ("Part Two" for "Part II"), so a query carrying the part finds only the packs that list it. When no row a query returns carries the requested title, the source tries the next query, ending with the title minus its trailing *, Part N* or *Volume N*. The strict match above still decides which part was found.
|
||||
|
||||
@@ -181,11 +181,14 @@ def _format_match(
|
||||
# Reject things that are clearly an ebook and nothing else.
|
||||
return _audiobook_signal(release, audiobook_formats) and fmt not in EBOOK_FORMAT_MARKERS
|
||||
|
||||
title_l = (release.title or "").lower()
|
||||
# A format named as a whole word anywhere in the name ("Dune.epub", "(epub)", "[EPUB]")
|
||||
# counts: Newznab releases, and Prowlarr ones outside MyAnonamouse, carry no format and
|
||||
# only the generic "book" content type, so the name is their only signal. Whole words
|
||||
# only, so "Mobile" is not "mobi".
|
||||
has_ebook_signal = (
|
||||
fmt in ebook_formats
|
||||
or (release.content_type or "").lower() == "ebook"
|
||||
or any(f".{ef}" in title_l for ef in ebook_formats)
|
||||
or not ebook_formats.isdisjoint(_tokens(release.title))
|
||||
)
|
||||
return has_ebook_signal and not _audiobook_signal(release, audiobook_formats)
|
||||
|
||||
|
||||
@@ -1196,6 +1196,68 @@ class TestContentTypeAwareMatching:
|
||||
|
||||
assert not auto_download.strict_match(release, _book(), content_type="ebook")
|
||||
|
||||
# Newznab releases, and Prowlarr ones outside MyAnonamouse, carry no format and the
|
||||
# generic "book" content type, so the format named in the name is the only signal.
|
||||
@staticmethod
|
||||
def _usenet(title):
|
||||
return _release(
|
||||
title=title,
|
||||
format=None,
|
||||
content_type="book",
|
||||
protocol=ReleaseProtocol.NZB,
|
||||
seeders=None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _ebook_format_match(release):
|
||||
return auto_download._format_match(
|
||||
release, "ebook", AUDIOBOOK_FORMATS, {"epub", "azw3", "mobi"}
|
||||
)
|
||||
|
||||
def test_a_usenet_release_naming_its_format_matches_an_ebook_request(self):
|
||||
release = self._usenet("Rick Rubin - The Creative Act (retail) (epub)")
|
||||
book = _book(title="The Creative Act", authors=["Rick Rubin"])
|
||||
|
||||
assert auto_download.strict_match(release, book, content_type="ebook")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"title",
|
||||
[
|
||||
"Matt Dinniman - Dungeon Crawler Carl (retail) (epub)",
|
||||
"Dungeon Crawler Carl [EPUB] - Matt Dinniman",
|
||||
"Matt Dinniman - Dungeon Crawler Carl - azw3",
|
||||
"Matt Dinniman - Dungeon Crawler Carl {MOBI}",
|
||||
],
|
||||
)
|
||||
def test_a_format_word_in_the_name_is_an_ebook_signal(self, title):
|
||||
assert self._ebook_format_match(self._usenet(title))
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"title",
|
||||
[
|
||||
"Matt Dinniman - Dungeon Crawler Carl (m4b)",
|
||||
"Matt Dinniman - Dungeon Crawler Carl (epub) (Unabridged)",
|
||||
],
|
||||
)
|
||||
def test_an_audiobook_signal_still_fails_an_ebook_request(self, title):
|
||||
assert not self._ebook_format_match(self._usenet(title))
|
||||
|
||||
def test_a_name_without_a_format_still_fails_an_ebook_request(self):
|
||||
assert not self._ebook_format_match(
|
||||
self._usenet("Matt Dinniman - Dungeon Crawler Carl (retail)")
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"title",
|
||||
[
|
||||
"Matt Dinniman - Dungeon Crawler Carl (Mobile Edition)",
|
||||
"Matt Dinniman - Dungeon Crawler Carl (Republished)",
|
||||
"Matt Dinniman - Dungeon Crawler Carl (epubs)",
|
||||
],
|
||||
)
|
||||
def test_a_word_containing_a_format_is_not_an_ebook_signal(self, title):
|
||||
assert not self._ebook_format_match(self._usenet(title))
|
||||
|
||||
def test_ebook_ranking_prefers_epub_then_azw3_then_mobi(self):
|
||||
mobi = _release(format="mobi", seeders=50)
|
||||
azw3 = _release(format="azw3", seeders=1)
|
||||
|
||||
Reference in new issue
Block a user