mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
feat: Implement filtering of unsupported download formats in CalibreOPDSClient
This commit is contained in:
@@ -4,6 +4,7 @@ import dataclasses
|
|||||||
import html
|
import html
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import PurePosixPath
|
||||||
from typing import Any, Dict, Iterable, List, Mapping, Optional
|
from typing import Any, Dict, Iterable, List, Mapping, Optional
|
||||||
from urllib.parse import urljoin, urlparse
|
from urllib.parse import urljoin, urlparse
|
||||||
from xml.etree import ElementTree as ET
|
from xml.etree import ElementTree as ET
|
||||||
@@ -38,6 +39,8 @@ _EPUB_MIME_TYPES = {
|
|||||||
"application/x-zip",
|
"application/x-zip",
|
||||||
"application/x-zip-compressed",
|
"application/x-zip-compressed",
|
||||||
}
|
}
|
||||||
|
_SUPPORTED_DOWNLOAD_MIME_TYPES = set(_EPUB_MIME_TYPES) | {"application/pdf"}
|
||||||
|
_SUPPORTED_DOWNLOAD_EXTENSIONS = {".epub", ".pdf"}
|
||||||
|
|
||||||
|
|
||||||
class CalibreOPDSError(RuntimeError):
|
class CalibreOPDSError(RuntimeError):
|
||||||
@@ -270,7 +273,12 @@ class CalibreOPDSClient:
|
|||||||
feed_id = root.findtext("atom:id", default=None, namespaces=NS)
|
feed_id = root.findtext("atom:id", default=None, namespaces=NS)
|
||||||
feed_title = root.findtext("atom:title", 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)
|
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)
|
return OPDSFeed(id=feed_id, title=feed_title, entries=entries, links=links)
|
||||||
|
|
||||||
def _parse_entry(self, node: ET.Element, base_url: str) -> OPDSEntry:
|
def _parse_entry(self, node: ET.Element, base_url: str) -> OPDSEntry:
|
||||||
@@ -576,14 +584,27 @@ class CalibreOPDSClient:
|
|||||||
results[key] = entry
|
results[key] = entry
|
||||||
return results
|
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
|
@staticmethod
|
||||||
def _select_download_link(links: Mapping[str, OPDSLink] | Iterable[OPDSLink]) -> Optional[OPDSLink]:
|
def _select_download_link(links: Mapping[str, OPDSLink] | Iterable[OPDSLink]) -> Optional[OPDSLink]:
|
||||||
if isinstance(links, Mapping):
|
if isinstance(links, Mapping):
|
||||||
iterable: List[OPDSLink] = list(links.values())
|
iterable: List[OPDSLink] = list(links.values())
|
||||||
else:
|
else:
|
||||||
iterable = list(links)
|
iterable = list(links)
|
||||||
|
supported = [link for link in iterable if CalibreOPDSClient._is_supported_download(link)]
|
||||||
best: Optional[OPDSLink] = None
|
best: Optional[OPDSLink] = None
|
||||||
for link in iterable:
|
for link in supported:
|
||||||
rel = (link.rel or "").lower()
|
rel = (link.rel or "").lower()
|
||||||
if "acquisition" not in rel:
|
if "acquisition" not in rel:
|
||||||
continue
|
continue
|
||||||
@@ -594,11 +615,8 @@ class CalibreOPDSClient:
|
|||||||
best = link
|
best = link
|
||||||
if best:
|
if best:
|
||||||
return best
|
return best
|
||||||
# Fallback to first epub-like link even without explicit acquisition rel
|
if supported:
|
||||||
for link in iterable:
|
return supported[0]
|
||||||
mime = (link.type or "").lower()
|
|
||||||
if mime in _EPUB_MIME_TYPES:
|
|
||||||
return link
|
|
||||||
# No valid acquisition-style link exposed
|
# No valid acquisition-style link exposed
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -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("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("/cover/1") == "http://example.com/cover/1"
|
||||||
assert client._make_url("?page=2") == "http://example.com/opds/?page=2"
|
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 = """<?xml version=\"1.0\" encoding=\"UTF-8\"?>
|
||||||
|
<feed xmlns=\"http://www.w3.org/2005/Atom\">
|
||||||
|
<id>catalog</id>
|
||||||
|
<title>Example Catalog</title>
|
||||||
|
<entry>
|
||||||
|
<id>audio-book</id>
|
||||||
|
<title>Unsupported Audio</title>
|
||||||
|
<link rel=\"http://opds-spec.org/acquisition\"
|
||||||
|
href=\"books/sample.mp3\"
|
||||||
|
type=\"audio/mpeg\" />
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<id>pdf-book</id>
|
||||||
|
<title>Allowed PDF</title>
|
||||||
|
<link rel=\"http://opds-spec.org/acquisition\"
|
||||||
|
href=\"books/sample.pdf\"
|
||||||
|
type=\"application/pdf\" />
|
||||||
|
</entry>
|
||||||
|
<entry>
|
||||||
|
<id>epub-book</id>
|
||||||
|
<title>Allowed EPUB</title>
|
||||||
|
<link rel=\"http://opds-spec.org/acquisition\"
|
||||||
|
href=\"books/sample.epub\" />
|
||||||
|
</entry>
|
||||||
|
</feed>
|
||||||
|
"""
|
||||||
|
|
||||||
|
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"))
|
||||||
|
|||||||
Reference in New Issue
Block a user