from abogen.integrations.calibre_opds import ( CalibreOPDSClient, OPDSEntry, OPDSFeed, feed_to_dict, ) def test_calibre_opds_feed_exposes_series_metadata() -> None: client = CalibreOPDSClient("http://example.com/catalog") xml_payload = """ catalog Example Catalog book-1 Sample Book The Expanse 4 """ feed = client._parse_feed(xml_payload, base_url="http://example.com/catalog") assert feed.entries, "Expected at least one entry in parsed feed" entry = feed.entries[0] assert entry.series == "The Expanse" assert entry.series_index == 4.0 feed_dict = feed_to_dict(feed) assert feed_dict["entries"][0]["series"] == "The Expanse" assert feed_dict["entries"][0]["series_index"] == 4.0 def test_calibre_opds_feed_extracts_series_from_categories() -> None: client = CalibreOPDSClient("http://example.com/catalog") xml_payload = """ catalog Example Catalog book-2 Network Effect """ feed = client._parse_feed(xml_payload, base_url="http://example.com/catalog") assert feed.entries, "Expected at least one entry in parsed feed" entry = feed.entries[0] assert entry.series == "The Murderbot Diaries" assert entry.series_index == 5.0 def test_calibre_opds_extracts_tags_and_rating_from_summary() -> None: client = CalibreOPDSClient("http://example.com/catalog") xml_payload = """ catalog Example Catalog book-3 Summary Sample 2024-01-15T00:00:00+00:00 RATING: ★★★½ TAGS: Science Fiction; Adventure SERIES: Saga [3] This is the detailed summary text. """ feed = client._parse_feed(xml_payload, base_url="http://example.com/catalog") entry = feed.entries[0] assert entry.series == "Saga" assert entry.series_index == 3.0 assert entry.tags == ["Science Fiction", "Adventure"] assert entry.rating == 3.5 assert entry.rating_max == 5.0 assert entry.summary == "This is the detailed summary text." assert entry.published == "2024-01-15T00:00:00+00:00" def test_calibre_opds_relative_urls_keep_catalog_prefix() -> None: client = CalibreOPDSClient("http://example.com/opds/") assert client._make_url("search") == "http://example.com/opds/search" 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 nav-author Authors (A) """ 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", "nav-author"} for entry in feed.entries: if entry.id.startswith("nav-"): assert entry.download is None assert entry.links, "Expected navigation entry to preserve links" else: assert entry.download is not None assert entry.download.href.endswith((".pdf", ".epub")) def test_calibre_opds_navigation_entries_without_download_are_preserved() -> None: client = CalibreOPDSClient("http://example.com/catalog") xml_payload = """ catalog Example Catalog nav-series Series """ feed = client._parse_feed(xml_payload, base_url="http://example.com/catalog") assert [entry.id for entry in feed.entries] == ["nav-series"] entry = feed.entries[0] assert entry.download is None assert any(link.href.endswith("/opds/series") for link in entry.links) def test_calibre_opds_search_filters_by_title_and_author() -> None: client = CalibreOPDSClient("http://example.com/catalog") feed = OPDSFeed( id="catalog", title="Catalog", entries=[ OPDSEntry(id="1", title="The Long Journey", authors=["Alice Smith"]), OPDSEntry(id="2", title="Hidden Worlds", authors=["Bob Johnson"]), OPDSEntry(id="3", title="Side Stories", authors=["Cara Nguyen"], series="Journey Tales"), ], ) filtered = client._filter_feed_entries(feed, "journey alice") assert [entry.id for entry in filtered.entries] == ["1"] filtered = client._filter_feed_entries(feed, "bob") assert [entry.id for entry in filtered.entries] == ["2"] filtered = client._filter_feed_entries(feed, "journey tales") assert [entry.id for entry in filtered.entries] == ["3"] filtered = client._filter_feed_entries(feed, "missing") assert filtered.entries == []