Skip to content

Hardcover metadata source, request/UI fixes, and QA/CI pipeline#1

Merged
Garrett3Nelson merged 12 commits into
mainfrom
feature/hardcover-and-fixes
Jun 28, 2026
Merged

Hardcover metadata source, request/UI fixes, and QA/CI pipeline#1
Garrett3Nelson merged 12 commits into
mainfrom
feature/hardcover-and-fixes

Conversation

@Garrett3Nelson

Copy link
Copy Markdown
Owner

Summary

Adds an optional Hardcover metadata source, fixes several request/discover/UI issues, and introduces the project's first automated QA gate (tests + GitHub Actions CI).

Features

  • Optional Hardcover API as a metadata source (replaces Open Library for search/discovery when a token is configured).

Fixes

  • Recover requests stuck on a transient download error.
  • Fix requests stuck on "Processing" after the file is imported.
  • Fix Bookshelf requests not downloading when the book already exists.
  • Fix Discover/Requests tab flicker, duplicate books, and foreign editions.
  • Increase metadata lookup timeouts to avoid read timeouts.

QA / CI (new)

  • pytest suite that exercises our own logic without mocking external book servers: boot/smoke, Open Library normalizers, client factory, JSON persistence round-trips, and a duck-typed client contract check.
  • LIBRESEERR_DATA_DIR env override so tests never touch real data/.
  • GitHub Actions: ruff + pytest on Python 3.12, plus a Docker build-and-boot job.
  • Lint cleanup: modern union type hints, sorted imports, timezone-aware datetimes.

Verification

CI green on the latest commit: Lint & test and Docker build & boot both pass.

🤖 Generated with Claude Code

Garrett3Nelson and others added 12 commits June 27, 2026 14:11
When Libreseerr POSTs a new author, Bookshelf's metadata refresh
immediately creates unmonitored edition records for all that author's
books. The subsequent book POST then collides (duplicate edition), so
add_book fell into its "already exists" paths and returned the record
without ever monitoring it or triggering a search — the book just sat
there and never downloaded.

Add _monitor_and_search(), used by both existing-book paths, which sets
the book monitored via the dedicated /book/monitor endpoint (the full
PUT /book/{id} crashes server-side on null editions) and fires a
BookSearch command. Also ensure the author is monitored via
/author/editor so RSS auto-grabs work for future books, without
touching individual book monitoring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019QAAfNHLFzwaD5CXerQU8g
Three issues on the Discover page:

- Flicker / results coming and going: the book cover onerror fallback
  pointed at via.placeholder.com, which is now defunct. Each broken
  cover re-set src to that dead host, which failed and re-fired onerror
  in an infinite loop, thrashing the network. Replace it with a
  self-contained inline SVG data-URI and null onerror so it can't loop.

- Same book shown several times: a popular work appears in multiple
  category rows. Dedupe across rows in loadDiscovery(), keeping the
  first occurrence.

- Foreign-language editions: add language=eng to the search.json
  discovery categories so sort=rating stops surfacing translations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019QAAfNHLFzwaD5CXerQU8g
The completion check relied solely on book.statistics.bookFileCount,
which Readarr/Bookshelf cache and don't recompute until an author
refresh runs. A freshly imported file therefore showed bookFileCount=0,
leaving the request stuck on "Processing" even though the file was on
disk and mapped to the book.

Add get_book_files() (GET /bookfile?bookId=) to the Readarr and
Bookshelf clients and consult it in refresh_requests: a request is
completed if the cached count is positive OR a book file actually
exists. LazyLibrarian returns an empty list (no per-book file endpoint;
it still completes via its snatched-list status check).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019QAAfNHLFzwaD5CXerQU8g
Add an optional Hardcover API token in Settings. When a token is
configured, Hardcover replaces Open Library for the Discover search box
and the discovery carousels; with no token, behaviour is unchanged.

Hardcover is the same provider Bookshelf uses for its own metadata, so
results line up better with what the backend can add, and it offers real
popularity/rating signals for the trending rows.

- hardcover.py: HardcoverClient (GraphQL) with test_connection,
  search_books (Typesense search), discover (Hasura books order_by for
  trending/best/new, search-term queries for genre rows), normalized into
  the shared book schema.
- app.py: get_metadata_client() gate; /api/search and /api/discover branch
  on the configured token; 5-minute discover cache keyed by (source,
  category) to stay under Hardcover's 60 req/min; admin GET/POST
  /api/hardcover and /api/hardcover/test endpoints.
- Settings UI: "Metadata Source - Hardcover" card with token, Test, Save.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019QAAfNHLFzwaD5CXerQU8g
Protect against ever pushing sensitive or noise files:
- /data/ holds the Hardcover token, server API keys, hashed passwords,
  and the session secret_key — must never be committed.
- __pycache__/*.pyc were tracked from upstream; untrack them.
- Ignore Claude/AI assistant files (CLAUDE.md, .claude/, .mcp.json) and
  common env/secret files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019QAAfNHLFzwaD5CXerQU8g
These were committed upstream; they are build artifacts and now ignored
via .gitignore. Removing from the index (kept on disk locally).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019QAAfNHLFzwaD5CXerQU8g
Book/author lookups and library listings proxy the server's upstream
metadata provider and are slow — several seconds when idle and well over
the previous 15s while the instance is busy refreshing (e.g. right after
a new, prolific author is added). Requesting a book by a not-yet-added
author could fail at the lookup stage with a read timeout before the
author was ever created.

Introduce LOOKUP_TIMEOUT (60s) in the Readarr and Bookshelf clients and
apply it to the lookup/list calls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019QAAfNHLFzwaD5CXerQU8g
renderRequest() used the same defunct via.placeholder.com fallback with a
self-referential onerror as the Discover cards did, so a broken/missing
request cover looped onerror against a dead host — the bouncing-text
flicker. Use the inline NO_COVER SVG and null onerror, matching the
Discover fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019QAAfNHLFzwaD5CXerQU8g
A stalled torrent ("download is stalled with no connections") marked a
request as error, and refresh then skipped any error request forever, so
it never recovered even after the file imported.

Re-evaluate everything except completed requests, and check for an
imported file first: if the book has a file, mark it completed
regardless of a stalled/seeding queue entry or a prior error. Clear the
stored error when a request moves to downloading/completed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019QAAfNHLFzwaD5CXerQU8g
Introduce a pytest suite that exercises our own logic without mocking the
external book servers: a boot/smoke check, Open Library normalizer units,
the client factory, JSON persistence round-trips, and a duck-typed client
contract check. Tests redirect the data dir via a new LIBRESEERR_DATA_DIR
env override so they never touch real data/.

Add a GitHub Actions workflow (ruff + pytest on Python 3.12, plus a Docker
build-and-boot job) and a ruff config. Apply the resulting lint fixes:
modern union type hints, sorted imports, and timezone-aware datetimes
(datetime.now(UTC) instead of deprecated utcnow()).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019QAAfNHLFzwaD5CXerQU8g
Use a host bind mount at /opt/appdata/libreseerr instead of a named volume,
set an explicit container_name, make the host port configurable via PORT,
and pass through an optional SECRET_KEY (auto-generated when blank).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019QAAfNHLFzwaD5CXerQU8g
The pytest console script (used in CI) does not put the repo root on
sys.path, so conftest's 'import app' failed with ModuleNotFoundError —
only 'python -m pytest' had been exercised locally. Set pythonpath=['.']
so imports work under either invocation.

Also bump checkout@v4->v5 and setup-python@v5->v6 to clear the Node 20
runner deprecation, and run pytest verbosely for clearer CI output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019QAAfNHLFzwaD5CXerQU8g
@Garrett3Nelson
Garrett3Nelson merged commit bc0b85d into main Jun 28, 2026
4 checks passed
@Garrett3Nelson
Garrett3Nelson deleted the feature/hardcover-and-fixes branch June 28, 2026 01:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant