From 67453ed17ce08002b8b76d39941d57a0f71e1d7c Mon Sep 17 00:00:00 2001 From: JB Date: Tue, 11 Nov 2025 07:29:13 -0800 Subject: [PATCH] feat: Implement filtering of unsupported download formats in CalibreOPDSClient --- abogen/integrations/calibre_opds.py | 32 ++++++++++++++++++------ tests/test_calibre_opds.py | 38 +++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/abogen/integrations/calibre_opds.py b/abogen/integrations/calibre_opds.py index f2ea3f4..ff425ef 100644 --- a/abogen/integrations/calibre_opds.py +++ b/abogen/integrations/calibre_opds.py @@ -4,6 +4,7 @@ import dataclasses import html import re from dataclasses import dataclass, field +from pathlib import PurePosixPath from typing import Any, Dict, Iterable, List, Mapping, Optional from urllib.parse import urljoin, urlparse from xml.etree import ElementTree as ET @@ -38,6 +39,8 @@ _EPUB_MIME_TYPES = { "application/x-zip", "application/x-zip-compressed", } +_SUPPORTED_DOWNLOAD_MIME_TYPES = set(_EPUB_MIME_TYPES) | {"application/pdf"} +_SUPPORTED_DOWNLOAD_EXTENSIONS = {".epub", ".pdf"} class CalibreOPDSError(RuntimeError): @@ -270,7 +273,12 @@ class CalibreOPDSClient: feed_id = root.findtext("atom:id", default=None, namespaces=NS) feed_title = root.findtext("atom:title", default=None, namespaces=NS) links = self._parse_links(root.findall("atom:link", NS), base_url) - entries = [self._parse_entry(node, base_url) for node in root.findall("atom:entry", NS)] + parsed_entries = [self._parse_entry(node, base_url) for node in root.findall("atom:entry", NS)] + entries = [ + entry + for entry in parsed_entries + if entry.download and self._is_supported_download(entry.download) + ] return OPDSFeed(id=feed_id, title=feed_title, entries=entries, links=links) def _parse_entry(self, node: ET.Element, base_url: str) -> OPDSEntry: @@ -576,14 +584,27 @@ class CalibreOPDSClient: results[key] = entry return results + @staticmethod + def _is_supported_download(link: OPDSLink) -> bool: + mime = (link.type or "").split(";")[0].strip().lower() + if mime in _SUPPORTED_DOWNLOAD_MIME_TYPES: + return True + href = (link.href or "").strip() + if not href: + return False + parsed_path = urlparse(href).path or "" + extension = PurePosixPath(parsed_path).suffix.lower() + return extension in _SUPPORTED_DOWNLOAD_EXTENSIONS + @staticmethod def _select_download_link(links: Mapping[str, OPDSLink] | Iterable[OPDSLink]) -> Optional[OPDSLink]: if isinstance(links, Mapping): iterable: List[OPDSLink] = list(links.values()) else: iterable = list(links) + supported = [link for link in iterable if CalibreOPDSClient._is_supported_download(link)] best: Optional[OPDSLink] = None - for link in iterable: + for link in supported: rel = (link.rel or "").lower() if "acquisition" not in rel: continue @@ -594,11 +615,8 @@ class CalibreOPDSClient: best = link if best: return best - # Fallback to first epub-like link even without explicit acquisition rel - for link in iterable: - mime = (link.type or "").lower() - if mime in _EPUB_MIME_TYPES: - return link + if supported: + return supported[0] # No valid acquisition-style link exposed return None diff --git a/tests/test_calibre_opds.py b/tests/test_calibre_opds.py index edd5975..22d4186 100644 --- a/tests/test_calibre_opds.py +++ b/tests/test_calibre_opds.py @@ -104,3 +104,41 @@ def test_calibre_opds_relative_urls_keep_catalog_prefix() -> None: assert client._make_url("books/sample.epub") == "http://example.com/opds/books/sample.epub" assert client._make_url("/cover/1") == "http://example.com/cover/1" assert client._make_url("?page=2") == "http://example.com/opds/?page=2" + + +def test_calibre_opds_filters_out_unsupported_formats() -> None: + client = CalibreOPDSClient("http://example.com/catalog") + xml_payload = """ + + catalog + Example Catalog + + audio-book + Unsupported Audio + + + + pdf-book + Allowed PDF + + + + epub-book + Allowed EPUB + + + + """ + + feed = client._parse_feed(xml_payload, base_url="http://example.com/catalog") + + identifiers = {entry.id for entry in feed.entries} + assert identifiers == {"pdf-book", "epub-book"} + for entry in feed.entries: + assert entry.download is not None + assert entry.download.href.endswith((".pdf", ".epub"))