Files
shelfmark/tests/core/test_metadata_search_library.py
T
splitsec2 37a77e9562 feat(library): mark search results already in a Calibre library (#1377)
Per discussion #1372, where you said you were fine with this specific
implementation: check whether metadata.db exists, read it if so, and
show a check mark saying the book is already there.

Searching for a book you already own gives no hint that you own it, so
the easiest way to end up with a second copy is to not remember you have
the first. This reads a Calibre `metadata.db`, read only, and marks
matching results with an **In library** badge in the card, list and
compact views and in the details dialog.

Off by default. It sits in Settings, General beside the existing Library
URL, with a test button that reports how many books it indexed. No HTTP
call, no token, nothing written back.

Matching runs most to least confident: a shared external id, then an
ISBN compared in both ISBN-10 and ISBN-13 form, then fuzzy title tokens
plus the author surname. The check fails open, so an unreadable database
degrades the badge and never blocks a search, and entries are cached for
ten minutes with an early refresh when the file changes, so a large
library costs one read rather than one per search.

`text_match.py` is new and shared by the index and the provider, so
title, author and ISBN matching stays consistent in one place.

## On the provider interface

`library_index` talks only to a `LibraryProvider` protocol and knows
nothing about Calibre. That is deliberate but it is not speculative
generality, it is what let me send you the Calibre half on its own: I
run an Audiobookshelf provider on the same interface in my fork, which
is where the audiobook side of the badge comes from. I have left that
out because it is a new service integration rather than something
already in the codebase, which is the line your non-goals draw. Happy to
send it separately if you ever want it, and equally happy for the answer
to be no.

Adding a library is a module with the `LibraryProvider` shape plus one
line in `all_providers()`.

## Verification

- `tests/core/test_library_index.py`: id, ISBN and fuzzy matching,
per-content-type provider selection, fail-open on provider errors,
stale-cache reuse, TTL and fingerprint refresh, per-provider cache
isolation, and the test-connection path including unsaved form values.
- `tests/core/test_text_match.py`: ISBN variants and token matching.
- `src/frontend/src/tests/libraryBadge.test.ts` and the added cases in
`bookTransformers.test.ts`.
- Python suite (3269) and frontend suite (206) green, plus ruff, ruff
format, basedpyright, vulture, tsc, oxlint, oxfmt and the production
build.
2026-09-25 18:18:10 -04:00

72 lines
2.4 KiB
Python

"""The metadata search and book endpoints carry per-format library ownership."""
from __future__ import annotations
import importlib
from unittest.mock import patch
import pytest
from shelfmark.metadata_providers import BookMetadata, SearchResult
@pytest.fixture(scope="module")
def main_module():
with patch("shelfmark.download.orchestrator.start"):
import shelfmark.main as main
importlib.reload(main)
return main
@pytest.fixture
def client(main_module):
return main_module.app.test_client()
class _StubProvider:
name = "stub"
display_name = "Stub"
search_fields: list[object] = []
def __init__(self, books: list[BookMetadata]) -> None:
self._books = books
def is_available(self) -> bool:
return True
def search_paginated(self, options):
return SearchResult(books=self._books, page=1, total_found=len(self._books), has_more=False)
def get_book(self, book_id: str) -> BookMetadata | None:
return next((b for b in self._books if b.provider_id == book_id), None)
def _search(main_module, client, ownership):
book = BookMetadata(provider="stub", provider_id="42", title="Dungeon Crawler Carl")
stub = _StubProvider([book])
with (
patch.object(main_module, "get_auth_mode", return_value="none"),
patch("shelfmark.metadata_providers.is_provider_registered", return_value=True),
patch("shelfmark.metadata_providers.is_provider_enabled", return_value=True),
patch("shelfmark.metadata_providers.get_provider_kwargs", return_value={}),
patch("shelfmark.metadata_providers.get_provider", return_value=stub),
patch("shelfmark.metadata_providers.get_configured_provider", return_value=stub),
patch("shelfmark.core.library_index.ownership", ownership),
):
response = client.get("/api/metadata/search?query=carl&provider=stub")
assert response.status_code == 200, response.get_json()
return response.get_json()["books"][0]
def test_search_results_carry_library_ownership(main_module, client):
result = _search(main_module, client, lambda book: {"ebook": True, "audiobook": False})
assert result["library"] == {"ebook": True, "audiobook": False}
def test_search_results_omit_library_when_no_check_is_enabled(main_module, client):
result = _search(main_module, client, lambda book: None)
assert "library" not in result