Skip to content

v1.30.3

Latest

Choose a tag to compare

@github-actions github-actions released this 12 Aug 19:17
· 1 commit to main since this release
ec43ca5

Six fixes. Three of them are one story: a book's media type records what Bindery
was told to go and fetch, but the book detail page was using it to decide what
to show you about the files already on disk. A book can legally hold both an
ebook and an audiobook, and when the two disagreed the page put an Audiobook
badge next to an epub's path, gave the second file no surface at all, and —
the part worth upgrading for — offered a Delete button that removed both
formats while naming one of them. The File section is now a list of what is
actually there, and every delete says exactly which paths it will remove.

The other three are things Bindery was doing repeatedly and silently: work it
had already done, requests it had already been refused, and a field it was
confidently reporting wrong.

The one most likely to be felt is the author refresh. Before queuing a search
for each book it creates, the sync checked whether the file was already on disk,
and that check walked the entire library — once per book. On local disk the OS
caches the directory tree and it hides; on a NAS mount it does not, and a
65-book refresh spent close to an hour doing nothing but re-reading the same
directories. It now walks each library root once per refresh.

The other two came from users noticing something odd and looking closer. An
indexer that answers "request limit reached, retry in 485 minutes" was being
asked again on the very next search, and every search for the following eight
hours, because nothing recorded what it had said. And a book whose file was
Spanish displayed as English forever, because the language shown came from the
metadata provider's description of the work and the file's own tag was only
ever read when the provider had supplied nothing.

Fixed

  • The book detail page now lists every file on the book, and a delete can no
    longer remove a format you were not shown

    (#1948) — media_type
    records acquisition intent, what search and monitoring are told to hunt for.
    book_files records inventory, what is on disk. The File section rendered
    inventory through the intent value, so any file outside the declared type was
    invisible to display but still included in destruction. A book marked
    audiobook that also held an epub showed a 🎧 Audiobook badge (from the media
    type) next to the epub's path (from the legacy file_path column, which was
    kept ebook-first regardless of media type); the audiobook itself had no row at
    all, because the format switcher only appeared for books already marked
    dual-format; Download sent no format and served the epub; and Delete file sent
    a format-less DELETE, which enumerates every registered file and removes
    both formats, while the dialog named one path and described it as the other
    format's. The badge and the path came from different sources, and the
    confirmation and the request disagreed.

    The section is now a list built from the files themselves, grouped by format,
    each group badged by its own format and nothing hidden behind the declared
    type. Download and Delete live on the format group and are always scoped to
    it, which is the honest unit: both endpoints act on every file of that format,
    plus, for delete, the same-name sibling sweep. The confirmation dialog lists
    every path the request will remove and is built from the same state the
    request is, so the two cannot drift apart again. A format-less delete is still
    available but only as an explicit Delete all files action, and its dialog
    lists every path across both formats. Fix match now moves the file whose
    row you opened rather than whichever format the switcher was on, and the
    switcher is gone: hiding one format behind it is what made a registered file
    invisible in the first place. One thing the switcher did carry is kept: a
    format the book wants but has no file for still shows as Not downloaded,
    so a dual-format book with one file on hand says which half it is still
    waiting on. Books that predate the book_files migration and were never
    re-imported still render from the legacy columns, and because that legacy
    single path carries no format of its own, its Download and Delete deal with
    it as the book's only file rather than guessing a format the server might
    disagree with.

    Two smaller things fell out of the same work. A new per-file Forget this
    file
    action drops a stale path from Bindery's records without touching disk
    — the database-only mode added in
    #1692 had no interface at
    all until now, which is what you want when a file has already been moved or
    removed elsewhere and the old path is still being reported. And the media
    badge was a two-way ebook/audiobook check, so a dual-format book displayed as
    "📖 Ebook"; it now renders both.

  • A book holding both formats now declares itself dual-format
    (#1946) — the display fix
    above makes the page correct whatever the media type says, but the media type
    was also simply wrong, and it is what search and monitoring read. When both an
    ebook and an audiobook are registered against a book, its media type is now
    widened to both on the next file event, because a file on disk settles the
    question of what the book is. This is driven by inventory and is deliberately
    independent of the metadata-driven widening pinned in
    #1732: that pin exists
    because Hardcover lists an audio edition for most popular titles, so widening
    from metadata alone was widening on a claim. Here the audiobook is already
    imported. Widening only ever fires when both files are present, so it cannot
    flip a book back to wanted or start a download. Affected books heal on their
    next import, delete, rename, or library reorganize; nothing runs at upgrade.

    Two related corrections ride along. The legacy file_path column, which is
    what the format-less download endpoint and OPDS serve, now prefers the path
    matching the book's media type instead of always taking the ebook. And the
    book list's mediaType=both filter now works: the ebook and audiobook
    filters deliberately include dual-format books, so neither of them isolated
    them, and the literal value both fell through unhandled and returned the
    entire library. The Books page has a 📖🎧 Both button to match.

  • Editing an unmonitored book no longer starts a download
    (#1947) — the book update
    endpoint fires an immediate indexer search whenever a book crosses into
    wanted: a status edit, a "Delete file", or a media-type change that exposes
    a format it does not have
    (#1148). The only thing
    guarding that was the global auto-grab kill-switch, so widening a book to
    dual-format grabbed the missing format even when the book was explicitly
    unmonitored — the one per-book control for "keep track of this, do not go and
    get it". It now honours monitored. The status still changes and the book
    still appears on the Wanted page; only the search is suppressed, and it runs
    as normal once you monitor the book. The twelve-hour wanted scan already
    honoured this, so nothing there changes.

  • An author sync no longer walks the entire library once per new book
    (#1888,
    #1929) — before queuing a
    search for each book it creates, the sync checks whether the file already
    exists on disk, and that check did a full recursive walk of every library
    root, per book. A sync that added 65 books walked the whole library 65 times.
    On local disk the OS caches the directory tree and the cost hides; on an NFS
    or SMB mount every walk is real network round trips per directory entry, and
    at a few dozen seconds per walk this alone accounts for the reported
    hour-long refresh. The sync now takes one snapshot of the library per
    refresh: each root is walked once, on first use, and every per-book check is
    answered from memory with the same matching rules as before — same root
    selection per media type, same author-folder pre-filter, same title and
    author comparison, in the same order. The walk also now honours cancellation,
    which it previously ignored, so deleting an author mid-refresh stops the
    filesystem work instead of letting it run to completion. One-off checks
    (adding a single book, series add, recommendations) keep their per-call walk
    and see the library exactly as it is at that moment; only files copied in by
    hand while a refresh is mid-flight are invisible to that refresh's snapshot,
    and the next refresh sees them.

  • A rate-limited indexer is left alone until it says to come back
    (#1934) — when an indexer
    answers a search with a Newznab 500 (Request limit reached. Retry in 485 minutes.) Bindery used to record nothing, so the next search and every search
    for the following eight hours sent it another request it had already refused.
    The rate-limit classification existed but was consulted only inside a single
    search, to stop the query cascade falling through to lower tiers. The retry
    hint is now parsed out of the indexer's own message and that indexer is
    skipped until the deadline passes, across the scheduled wanted scan, on-add
    and bulk searches, and interactive search alike — they share one searcher, so
    a limit hit by one is respected by all of them. An indexer that gives no hint
    gets an hour; a parsed hint is clamped to between a minute and a day so a
    malformed or absurd value cannot bench an indexer indefinitely. Editing the
    indexer clears the hold immediately, so a new API key or a different account
    takes effect on the next search rather than waiting out a lockout that
    belonged to the old configuration. Interactive search reports the held indexer
    as skipped with the deadline in the Search details panel, in the same place
    the original error appeared, rather than dropping it from the list.

    Two related gaps are deliberately untouched here and tracked separately:
    authentication failures (a suspended account, a revoked key) get no cooldown,
    because they never heal on a timer and a user who fixes their key must see it
    work immediately — those need visibility and a notification
    (#1935) — and auto-grab
    still decides from whatever indexers answered without recording that the
    others were unreachable
    (#1936). The cooldown is
    held in memory, so a restart costs one refused request per indexer per search
    before it re-learns the limit, which is exactly the old behaviour and never
    worse.

  • A book whose file is in a different language than the catalogue says now
    gets corrected on import

    (#1933) — the language on a
    book page came from the metadata provider, which describes the abstract
    work, not the file you actually hold. The embedded dc:language tag was
    read only when the provider had supplied nothing at all, so a Spanish EPUB
    imported against an English OpenLibrary record displayed "English"
    indefinitely, with the release name buried in a history row the only hint
    otherwise. The tag is now read on every EPUB import and, when it disagrees
    with the stored value, the file wins: a work has editions in many languages,
    but the file on disk is one specific edition and is the thing you open. The
    correction is recorded as a Language Corrected history event showing both
    codes, so "why does my English book read as Spanish" has an answer in the
    place you would look for it rather than only in a log line.

    Precedence is user, then file, then provider. A language you set by hand locks
    the field, and a locked field is left alone — the EPUB is not even opened.
    Filling a language the catalogue never had is unchanged and stays silent: it
    is a gap being filled, not a disagreement, and OpenLibrary routinely supplies
    no work-level language at all. Comparison normalises both sides first, so a
    provider's en against a file's eng is not mistaken for a conflict, and
    nothing is written when an import fails — a book that never landed must not
    rewrite its own catalogue entry.

Internal

  • Race guard on the metadata test doubles
    (#1924) — mockProvider's
    call recorders were written from several goroutines once the author refresh
    started fanning out, which the race detector caught intermittently. Test-only;
    no runtime behaviour changes.
  • Base image digests bumped for the telemetry-server and discord-stats
    helper containers (#1925,
    #1926). The Bindery image
    itself is unchanged.