mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-11-15 15:58:12 +00:00
fix(prowlarr): hide results whose title extension is not a supported format (#1490)
Closes #1481 ## Problem Plain (non-enriched) indexers such as Zamunda, Knaben or LimeTorrents return titles like `Иван Вазов - Под игото.doc`. They show next to the epubs. Picking one downloads the file, then post-processing discards it because `doc` is not in `SUPPORTED_FORMATS`. ## Change - `_unsupported_title_extension()` reads a trailing file extension from the release title. When it is a known file format that is not in Supported Book Formats, the result is hidden. - Kept: titles with no extension, `[EPUB]`-style tags, dotted titles like `Jane.Eyre.by.Bronte` (only known file extensions count), archives (`zip`, `rar`, `7z`), and audiobook searches. - New Prowlarr setting `PROWLARR_HIDE_UNSUPPORTED_FORMATS` ("Hide results in formats you did not enable"), default on. - One info log line per search with the hidden count; a debug line per hidden title. - `docs/environment-variables.md`: added only the new setting's rows. Running `scripts/generate_env_docs.py` on current `main` also changes unrelated lines (`SHELFMARK_API_KEY_READONLY`, the slskd anchor), so I left those alone. ## Tests - New `TestHideUnsupportedFormats` in `tests/prowlarr/test_source.py`: the extension helper (parametrised), audiobook bypass, comma-separated config, and a full `search()` with the setting on and off. - `tests/prowlarr`: 784 passed. Ruff lint/format, vulture and basedpyright on the changed files are clean. - Full suite on macOS: the same 84 bypass/entrypoint failures as on `main` (browser extra and Linux permissions), none new. I ran an earlier version of this as a local patch on v1.4.0 against a real Prowlarr + Zamunda setup: the `.doc` row is gone and epubs stay. Co-authored-by: CaliBrain <calibrain@l4n.xyz>
This commit is contained in:
1 parent
d42494c696
commit
fc8a5b9230
4 files changed
+183
No files matched your search
@@ -1348,6 +1348,7 @@ How long to cache individual book details. Default: 600 (10 minutes). Max: 60480
|
||||
| `PROWLARR_EBOOK_MAX_SIZE_MB` | Hide ebook results larger than this. Expanded searches and general indexers can return TV series, courses and software next to the books. 0 shows everything, and a result with no reported size is always shown. Audiobook searches are not limited. | number | `0` |
|
||||
| `PROWLARR_AUTO_EXPAND` | Automatically retry search without category filtering if no results are found | boolean | `false` |
|
||||
| `PROWLARR_COLLAPSE_DUPLICATES` | Collapse a release that several indexer entries returned down to a single row, keeping the entry with the best Prowlarr priority. Turn this off to see every entry that carried it, which is what makes results from filter-specific entries (freeleech and the like) visible. | boolean | `true` |
|
||||
| `PROWLARR_HIDE_UNSUPPORTED_FORMATS` | Hide a result whose title ends in a file extension that is not in Supported Book Formats, such as a .doc or .rtf file. Downloading it would only fail at post-processing. Titles without an extension, archives and audiobook searches are never hidden. | boolean | `true` |
|
||||
| `PROWLARR_USE_SEED_PREFERENCES` | Apply per-indexer seed time and ratio preferences from Prowlarr when sending torrents to the download client | boolean | `false` |
|
||||
| `PROWLARR_MAM_ID` | The mam_id value MyAnonamouse shows when you create a session. MAM locks each session to one IP or ASN, so reusing Prowlarr's or another client's session often fails with a 403: you will likely need a separate session for Shelfmark if it reaches MAM from another IP (different host, VPN or proxy) or the existing session is ASN-locked to another network. Leave empty to turn enrichment off. | string (secret) | _none_ |
|
||||
|
||||
@@ -1430,6 +1431,15 @@ Collapse a release that several indexer entries returned down to a single row, k
|
||||
- **Type:** boolean
|
||||
- **Default:** `true`
|
||||
|
||||
#### `PROWLARR_HIDE_UNSUPPORTED_FORMATS`
|
||||
|
||||
**Hide results in formats you did not enable**
|
||||
|
||||
Hide a result whose title ends in a file extension that is not in Supported Book Formats, such as a .doc or .rtf file. Downloading it would only fail at post-processing. Titles without an extension, archives and audiobook searches are never hidden.
|
||||
|
||||
- **Type:** boolean
|
||||
- **Default:** `true`
|
||||
|
||||
#### `PROWLARR_USE_SEED_PREFERENCES`
|
||||
|
||||
**Use Prowlarr seed preferences**
|
||||
|
||||
@@ -259,6 +259,18 @@ def prowlarr_config_settings() -> list[SettingsField]:
|
||||
),
|
||||
show_when={"field": "PROWLARR_ENABLED", "value": True},
|
||||
),
|
||||
CheckboxField(
|
||||
key="PROWLARR_HIDE_UNSUPPORTED_FORMATS",
|
||||
label="Hide results in formats you did not enable",
|
||||
default=True,
|
||||
description=(
|
||||
"Hide a result whose title ends in a file extension that is not in Supported "
|
||||
"Book Formats, such as a .doc or .rtf file. Downloading it would only fail at "
|
||||
"post-processing. Titles without an extension, archives and audiobook "
|
||||
"searches are never hidden."
|
||||
),
|
||||
show_when={"field": "PROWLARR_ENABLED", "value": True},
|
||||
),
|
||||
CheckboxField(
|
||||
key="PROWLARR_USE_SEED_PREFERENCES",
|
||||
label="Use Prowlarr seed preferences",
|
||||
|
||||
@@ -348,6 +348,64 @@ AUDIOBOOK_FORMATS = list(CORE_AUDIOBOOK_FORMATS)
|
||||
# Combined list for format detection (audiobook formats first for priority)
|
||||
ALL_BOOK_FORMATS = AUDIOBOOK_FORMATS + EBOOK_FORMATS
|
||||
|
||||
# A trailing file extension on a release title, e.g. "Author - Title.doc".
|
||||
_TITLE_EXTENSION_PATTERN = re.compile(r"\.([a-z][a-z0-9]{1,4})\s*$")
|
||||
|
||||
# Extensions that are known to be files. Only these are hidden, so dotted titles
|
||||
# like "Jane.Eyre.by.Bronte" are not read as a ".bronte" file, and archive
|
||||
# containers (zip, rar, 7z) are never hidden.
|
||||
_KNOWN_FILE_EXTENSIONS = frozenset(ALL_BOOK_FORMATS) | {
|
||||
"azw4",
|
||||
"chm",
|
||||
"djv",
|
||||
"doc",
|
||||
"docx",
|
||||
"htm",
|
||||
"html",
|
||||
"kfx",
|
||||
"lrf",
|
||||
"lrx",
|
||||
"md",
|
||||
"odt",
|
||||
"oxps",
|
||||
"prc",
|
||||
"rtf",
|
||||
"snb",
|
||||
"tcr",
|
||||
"tpz",
|
||||
"wps",
|
||||
"xps",
|
||||
}
|
||||
|
||||
# Same default as the Supported Book Formats setting and post-processing.
|
||||
_DEFAULT_SUPPORTED_FORMATS = ("epub", "mobi", "azw3", "fb2", "djvu", "cbz", "cbr")
|
||||
|
||||
|
||||
def _unsupported_title_extension(title: str, content_type: str) -> str | None:
|
||||
"""Return the trailing extension of a title when it is a format the user did not enable.
|
||||
|
||||
Indexers that are not enriched return plain titles, so a single-file release
|
||||
such as "Author - Title.doc" is only recognisable by its extension. Picking it
|
||||
would download a file that post-processing then discards.
|
||||
"""
|
||||
if content_type == "audiobook":
|
||||
return None
|
||||
match = _TITLE_EXTENSION_PATTERN.search(str(title or "").lower())
|
||||
if not match:
|
||||
return None
|
||||
extension = match.group(1)
|
||||
if extension not in _KNOWN_FILE_EXTENSIONS:
|
||||
return None
|
||||
configured = config.get("SUPPORTED_FORMATS", None)
|
||||
if isinstance(configured, str):
|
||||
configured = configured.split(",")
|
||||
if not isinstance(configured, (list, tuple, set)) or not configured:
|
||||
configured = _DEFAULT_SUPPORTED_FORMATS
|
||||
supported = {str(fmt).strip().lower() for fmt in configured}
|
||||
if extension in supported:
|
||||
return None
|
||||
return extension
|
||||
|
||||
|
||||
# Backend safeguard: cap total Prowlarr search time per request.
|
||||
PROWLARR_SEARCH_TIMEOUT_SECONDS = 120.0
|
||||
@@ -1298,6 +1356,8 @@ class ProwlarrSource(ReleaseSource):
|
||||
# A manual query is the user's own words; ranking it against the
|
||||
# metadata author would second-guess what they typed.
|
||||
wanted_author = "" if plan.manual_query else plan.author
|
||||
hide_unsupported_formats = config.get("PROWLARR_HIDE_UNSUPPORTED_FORMATS", True)
|
||||
hidden_unsupported = 0
|
||||
|
||||
max_bytes = _max_release_bytes(content_type)
|
||||
oversized = 0
|
||||
@@ -1319,6 +1379,18 @@ class ProwlarrSource(ReleaseSource):
|
||||
content_type,
|
||||
enable_format_detection=is_enriched,
|
||||
)
|
||||
if hide_unsupported_formats:
|
||||
unsupported_extension = _unsupported_title_extension(
|
||||
release.title, content_type
|
||||
)
|
||||
if unsupported_extension:
|
||||
hidden_unsupported += 1
|
||||
logger.debug(
|
||||
"Prowlarr: hiding '%s', .%s is not a supported format",
|
||||
release.title,
|
||||
unsupported_extension,
|
||||
)
|
||||
continue
|
||||
if idx_id_int is not None and idx_id_int in indexer_priority:
|
||||
release.extra["indexer_priority"] = indexer_priority[idx_id_int]
|
||||
results.append(release)
|
||||
@@ -1332,6 +1404,11 @@ class ProwlarrSource(ReleaseSource):
|
||||
mam_releases.append(release)
|
||||
mam_indexer = mam_indexer or mam_indexers[idx_id_int]
|
||||
|
||||
if hidden_unsupported:
|
||||
logger.info(
|
||||
"Prowlarr: hid %s result(s) in a format that is not enabled",
|
||||
hidden_unsupported,
|
||||
)
|
||||
if oversized:
|
||||
logger.debug("Prowlarr: hid %s result(s) over the ebook size limit", oversized)
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ from shelfmark.release_sources.prowlarr.source import (
|
||||
_release_identity,
|
||||
_result_dedup_key,
|
||||
_search_budget_seconds,
|
||||
_unsupported_title_extension,
|
||||
)
|
||||
from shelfmark.release_sources.prowlarr.utils import (
|
||||
build_source_id,
|
||||
@@ -1537,6 +1538,89 @@ class TestProwlarrStaleIndexerSelection:
|
||||
assert fake_client.searched_indexer_ids == []
|
||||
|
||||
|
||||
class TestHideUnsupportedFormats:
|
||||
"""Plain indexer titles that end in a format the user did not enable are hidden."""
|
||||
|
||||
SUPPORTED = ["epub", "mobi", "azw3"]
|
||||
|
||||
def _patch_config(self, monkeypatch, overrides=None):
|
||||
import shelfmark.release_sources.prowlarr.source as prowlarr_source
|
||||
|
||||
values = {
|
||||
"PROWLARR_INDEXERS": "",
|
||||
"PROWLARR_AUTO_EXPAND": False,
|
||||
"SUPPORTED_FORMATS": self.SUPPORTED,
|
||||
**(overrides or {}),
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
prowlarr_source.config, "get", lambda key, default=None: values.get(key, default)
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("title", "expected"),
|
||||
[
|
||||
("Иван Вазов - Под игото.doc", "doc"),
|
||||
("Author - Title.RTF", "rtf"),
|
||||
("Author - Title.pdf ", "pdf"),
|
||||
("Author - Title.epub", None),
|
||||
("Author - Title [EPUB]", None),
|
||||
("Jane.Eyre.by.Bronte", None),
|
||||
("Author - Title.rar", None),
|
||||
("Author - Title.zip", None),
|
||||
("Author - Title", None),
|
||||
("", None),
|
||||
],
|
||||
)
|
||||
def test_unsupported_title_extension(self, monkeypatch, title, expected):
|
||||
self._patch_config(monkeypatch)
|
||||
assert _unsupported_title_extension(title, "ebook") == expected
|
||||
|
||||
def test_audiobook_searches_are_not_filtered(self, monkeypatch):
|
||||
self._patch_config(monkeypatch)
|
||||
assert _unsupported_title_extension("Author - Title.doc", "audiobook") is None
|
||||
|
||||
def test_comma_separated_supported_formats(self, monkeypatch):
|
||||
self._patch_config(monkeypatch, {"SUPPORTED_FORMATS": "epub, doc"})
|
||||
assert _unsupported_title_extension("Author - Title.doc", "ebook") is None
|
||||
assert _unsupported_title_extension("Author - Title.rtf", "ebook") == "rtf"
|
||||
|
||||
def _search(self, monkeypatch, overrides=None):
|
||||
self._patch_config(monkeypatch, overrides)
|
||||
fake_client = FakeTorznabClient(
|
||||
search_results=[
|
||||
{
|
||||
"guid": f"result-{index}",
|
||||
"protocol": "torrent",
|
||||
"title": title,
|
||||
"magnetUrl": f"magnet:?xt=urn:btih:abc{index}",
|
||||
"indexerId": 1,
|
||||
"indexer": "Zamunda",
|
||||
}
|
||||
for index, title in enumerate(
|
||||
["Иван Вазов - Под игото.epub", "Иван Вазов - Под игото.doc"]
|
||||
)
|
||||
]
|
||||
)
|
||||
source = ProwlarrSource()
|
||||
monkeypatch.setattr(source, "_get_client", lambda: fake_client)
|
||||
book = BookMetadata(
|
||||
provider="hardcover", provider_id="123", title="Под игото", authors=["Иван Вазов"]
|
||||
)
|
||||
|
||||
from shelfmark.core.search_plan import build_release_search_plan
|
||||
|
||||
plan = build_release_search_plan(book, languages=["bg"])
|
||||
return source.search(book, plan, content_type="ebook")
|
||||
|
||||
def test_search_hides_unsupported_format(self, monkeypatch):
|
||||
releases = self._search(monkeypatch)
|
||||
assert [release.title for release in releases] == ["Иван Вазов - Под игото.epub"]
|
||||
|
||||
def test_search_keeps_unsupported_format_when_disabled(self, monkeypatch):
|
||||
releases = self._search(monkeypatch, {"PROWLARR_HIDE_UNSUPPORTED_FORMATS": False})
|
||||
assert len(releases) == 2
|
||||
|
||||
|
||||
class TestEbookMaxSize:
|
||||
"""PROWLARR_EBOOK_MAX_SIZE_MB hides oversized ebook results and nothing else (#844)."""
|
||||
|
||||
|
||||
Reference in new issue
Block a user