Conversation
Co-authored-by: fallenbagel <98979876+fallenbagel@users.noreply.github.com>
Media.getMedia() loads the requests and issues relations for GET /movie/:id and GET /tv/:id, and the User relations hanging off them are eager, so every authenticated user could read any other user's email and plexId out of mediaInfo. The same payload carried full issue contents, including comment bodies, to viewers who are refused GET /issue. Media.filter() now strips the nested users and withholds the issues from anyone lacking the perms that the endpoing requires, and drops the issue fields nothing reads. Reported as a security advisory, GHSA-xc6j-6p6h-p799.
The series header was a bespoke stack of a toggle, a rule and a pair of labels, which looked nothing like the format picker a single book request opens. It is now the same table: a toggle, the format, and its status, headed Full Series so it is clear the row covers every book. The status column reports the series as a whole, available only when every book is, and partly available when some are. The books below keep their own table, now headed Books.
…ns them A book switched off, or a series with every book switched off, still showed its format toggles lit and clickable, so the modal claimed formats that were not going to be requested. Those toggles now read off and are inert while the switch above them is off, and come back to their previous values when it is turned on again: the choice is still held, it is only hidden while nothing can act on it. A book's own switch also moves onto the format row, divided from it by a rule, rather than sitting above the cover.
A book's switch stayed on when the formats above had been cleared, so it claimed a book that nothing was going to fetch, and its format toggles sat there greyed with nothing to act on. The switch now reads on only when the book really is in the request, which means clearing the formats above reads through to every book, and a book's formats are rendered only while its switch is on rather than shown disabled. Switching a book back on restores its formats, falling back to everything it can take when the formats above are off as well. Books are also flush left with the full width for their titles, and the switch is labelled Request to say what it does.
…luded A book with nothing left to request was exempted from the switch above it, so clearing the formats turned every other book off and left those two on with their formats still listed, which read as the header not working. The exemption is gone: a book's switch reads on only while the book is in the request, so the formats above now govern the whole list. Its own label is dropped, leaving the switch beside the formats it controls.
Making every book follow the formats above hid the status of the books that are already available, leaving them as a bare title under a switch that only said they could not be requested. A book with nothing left to request shows its switch on and lists what it already holds again. Its switch stays inert, so this says what the book has rather than offering to fetch it.
Switching a book on while the formats above were all off gave that book formats of its own. That silently detached it: from then on it ignored the formats chosen for the rest of the series, so changing them moved every book but that one. A book is only ever given formats of its own by changing its own format switches. Switching it on now just puts it back in, and a book that was emptied rejoins the formats above. A switch with nothing above it to take is inert rather than inventing something to take.
Hiding a book's formats while its switch was off took the status of each format away with them, so a book you were not requesting said nothing about what it already had. The formats stay on show. Only their switches go inert, following the switch beside them, while the labels and status keep their full weight.
Hardcover files dramatized adaptations, split editions and bonus chapters under the book they belong to, numbered off its position, so a seven book series listed seventeen entries. Ten of them were editions of a book already in the list, which crowded the series page and its request. Only whole positions are the series' own books now. The rest stay reachable through search and keep their own pages, and the series status no longer counts editions that will never be held against it.
Dropping every fractional position left dramatized adaptations, split editions and bonus chapters out of the series entirely, so none of them could be requested from it. They are kept and marked as editions instead. The series page lists them in an Editions & Extras section under the series' own books, and the request modal gives them their own table where they start switched off. Full Series, the book count and the series status all cover the series' own books, so an edition is only requested when it is chosen individually.
Brings seerr up to v3.5.0. Conflicts were resolved by combining both sides: - MediaRequest: upstream moved override rules into overrideRules(), typed for TMDB media only. The fork's inline version never applied a rule to a book (its filter returned false for BookDetails), so books skip the new call and behave as before. metadataProfileId was only ever reassigned on that path, so it is now const. - MediaRequestSubscriber: upstream made updateParentStatus, sendToRadarr and sendToSonarr take the request's EntityManager. sendToReadarr follows suit: its synchronous reads and writes join the transaction, and the add-book callback, which runs after commit, reaches the database directly, as the Radarr path does. - cache: upstream replaced node-cache with lru-cache and requires a key bound on every tier. Readarr is bounded like Radarr and Sonarr, since it only holds profile and root folder lookups. Hardcover gets 1000 keys, above the size of a Readarr library, because scans look every book up there and evicting mid-scan would re-fetch at Hardcover's rate limit. - useDiscover, MediaSlider: upstream stopped the hide-available and hide-blocklisted filters dropping everything that is not a movie or series. Books keep the filters, and non-media results are no longer dropped. - RequestCard, RequestItem: upstream's precomputed download status, with the fork's book title kept. - AdvancedRequester: books stay in the type union alongside upstream's tmdbId. - seerr-api.yml: upstream's approve and decline responses, then the book path. - release.yml stays removed, as the fork removed it.
Brings develop up to seerr v3.5.0 through the synced upstream branch, where the upstream conflicts were resolved. - Media: getRelatedMedia keeps the requests join that per-format book chips rely on to tell requested apart from monitored, alongside upstream's cleanup of the line it sits on. - discover: upstream now validates sortBy against each media type's own sort options instead of accepting any string. The books route, which read sortBy from the shared schema, gets a schema of its own on the same pattern, backed by the six orders the Hardcover query already knows.
A cache miss returned the original download and optimized it in the background, so the first viewer of every cover received the full-size file, four to ten times the size of the optimized copy for Hardcover covers, and their browser kept it for the whole cache lifetime. Wait for the transform and fall back to the original only when it fails.
The audiobook section only offered its remove button when the media sat on the default 4K Sonarr server, a check with no book branch, so it never matched a book and an audiobook could only be deleted from Readarr by hand. Check the default audiobook Readarr server instead, and title the ebook section "Ebook Media" so the two sections read as a pair.
A book's two remove buttons read "Remove from Readarr" and "Remove from Audiobook Readarr", which left the ebook one unlabelled. They now read "Remove Ebook from Readarr" and "Remove Audiobook from Readarr". Movies and series keep their wording.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Brings
feat-readarrup to date with upstream seerr v3.5.0 and fixes a set of book-specific bugs found while running this fork in production against Chaptarr (a Readarr fork) with separate ebook and audiobook instances.Every change below also exists as a branch on
benjitobz/seerr, named in brackets, if you would rather cherry-pick individual pieces than take the whole thing. Most are cut fromfeat-readarrand stand alone. Seven were cut from this combined branch instead: the two request pickers,fix/book-request-format-badge,fix/readarr-none-metadata-profile,fix/series-book-format-status,fix/series-primary-booksandfix/image-proxy-stale-optimize. For those, cherry-pick the branch's own commits, after any entries they build on. The pickers needfeature/sync-book-format-requestsandfeature/audiobook-tile-statusfirst.Upstream sync
[sync/upstream-main]Merges
seerr-team/seerrmain through v3.5.0, in two steps: v3.4.1, then the v3.5.0 tag.At v3.4.1, three conflicts were resolved by combining both sides rather than picking one:
MediaRequest.ts— book quota checks folded into upstream's newignoreQuotalogic, so books participate in quota bypass like movies and series.routes/media.ts— kept upstream'sarrNamerefactor while preserving book-aware Readarr/Audiobook service naming.ManageSlideOver— kept both sides' new i18n strings.v3.5.0 changed several things the book code relies on, mostly without a textual conflict, so each needed a matching change on the book side:
node-cachewas replaced bylru-cache, which requires a size bound on every cache. The Readarr cache holds 64 entries and the Hardcover cache 1,000. The Hardcover bound has to exceed a library's size, or a scan evicts entries it will need again while held to 1 request per second.overrideRules(), typed for TMDB media. Books skip the call. The fork's inline filter already rejected every book, so no rule has ever applied to one.sendToReadarrnow reads through the request'sEntityManager, as the Radarr and Sonarr paths do. Its callback after the Readarr add uses the global repository, because it runs after the transaction commits.sortByis now validated per media type, so book discovery has its own schema listing the book sort keys.Media.getRelatedMediakeeps its join onmedia.requests, which the per-format chips need.Upstream's migrations are unchanged; on a production database the status remap changed no rows. The fork's removal of
.github/workflows/release.ymlwas preserved.Bug fixes
Book status could never change once available
[fix/book-status-downgrade]—baseScanner.processBookwrapped its entire status-update block inif (status !== AVAILABLE), so once a book was marked available its status was frozen. Deleting the files in Readarr, unmonitoring the book, or clearing the library left seerr reporting "Available" indefinitely. The guard now also runs when Readarr reports no files, downgrading toPROCESSINGwhile the book is still monitored/grabbed andUNKNOWNotherwise. Deliberately scoped toprocessBook; the identical block inprocessMovieis untouched, since movies get deletion handling fromavailabilitySync, which has no book support.Orphan cleanup ignored available books
[fix/book-status-cleanup]—cleanupOrphanedBooksonly queried books with statusPROCESSING, so a book that was available and then removed from Readarr entirely was never reset. Now coversPROCESSING,AVAILABLE, andPARTIALLY_AVAILABLE.Removals took up to 24h to surface
[fix/book-deletion-fast-detect]— cleanup was gated behind!isRecentOnly, so only the daily full scan noticed deletions. The recently-added scan already fetches Readarr's complete book list, so it now records that list and runs the same cleanup. Safe by construction: an unreachable Readarr throws before cleanup runs, so an outage cannot wipe statuses.Noisy "Invalid Hardcover ID" warnings
[fix/scanner-invalid-hcid-noise]— reported by a user with a large catalog. Books with an unparseableforeignBookIdare now skipped silently when unmonitored, ungrabbed, and fileless; monitored, grabbed, or downloaded books still warn. UsesbookFileCount > 0rather thanpercentOfBooks, matching the existing note inprocessReadarrBookabout forks wherepercentOfBooksreports 0 for unmonitored books that have files.Audiobook-only availability missing from media lists
[fix/media-list-4k-availability]— the media route's status filter only matchedstatus, so a book available solely as an audiobook (and a movie available solely in 4K) never appeared in Recently Added. It now matches either axis.Hardcover rate limiting
[fix/hardcover-rate-limit]— the client was throttled to 5 req/s while Hardcover allows 60/min, so cold-cache discover loads reliably hit 429 and surfaced as a 500 on the popular-books slider. Now 1 req/s. A follow-up on the same branch fixes the larger problem: every route built its own Hardcover client and the axios rate limiter lived on the client, so the limit only ever applied within a single request — a quickly-typed search, or a page of uncached book cards, sent its requests together and got 429s anyway. All clients now share one rate-limited instance per API key so bursts queue, and a 429 is retried up to twice honouringRetry-After(capped at 10s, falling back to 2s then 4s), which covers other consumers spending the same token's allowance.Book request button shown without book permission
[fix/book-titlecard-request-permission]—TitleCardgated the button onREQUEST_MOVIEfor books, so any user with movie-request rights saw it. The server correctly rejected the request, but the button should not appear; it is now gated onREQUEST_BOOK.Book quota selector mislabeled
[fix/book-quota-label]— the per-user book quota selector read "Series Request Limit"; now "Book Request Limit".Permission labels did not name books
[fix/book-permission-labels]— the book permission rows in the permission editor reused the generic wording, so an administrator could not tell which toggle governed ebooks and which audiobooks. Both are now named explicitly.Transient fetch errors flashed "Not Found"
[fix/transient-error-grace]— on cold loads the per-book detail fetches queue behind the Hardcover throttle, and a first-attempt failure briefly rendered the error card before SWR's retry succeeded. Title cards, request cards, and the book detail page now hold their loading state for 20s while retries run, and sliders keep their skeletons through transient errors.Readarr's "None" metadata profile offered as a request option
[fix/readarr-none-metadata-profile]— Readarr recreates an undeletable profile named "None" on every start, whose popularity threshold of 1e10 rejects every book. It exists so an author can be tracked with none of their books, which is never a valid choice for a request: picking it would add the book under a profile that hides it, leaving a request that can never complete. It is also typed General, so it appeared under both media types. It is now filtered out of the request panel and the server settings picker by name — filtering on the General profile type instead would also drop legitimate shared profiles.Ebook side of a book request left unlabelled
[fix/book-request-format-badge]— a request block badged the audiobook format but nothing else, so the two rows of a book requested in both formats read as one labelled and one unlabelled request. The ebook side is now named too.Series status and card chips did not name the format
[fix/series-book-format-status]— a series held in both formats rendered "Partially Available" twice with nothing saying which format each badge referred to: the first badge was missing its media type, andStatusBadgedrops the format prefix for books when partially available, which only reads correctly on a book page where one format is shown at a time.StatusBadgegains an opt-in to keep the prefix and the series page uses it for both badges. The series' book cards were also passed only the ebook status, so their audiobook chip never appeared; they now receive the audiobook status and the requests behind it.Series listed editions and extras as books of the series
[fix/series-primary-books]— Hardcover places a series' dramatized adaptations, split editions ("Part 1 of 2") and bonus chapters at fractional positions next to the books themselves. The series page therefore listed them as books of the series and counted them in its book count and status, and a series request selected them. Books at fractional positions are now flagged as extras. The series page lists them in their own "Editions & Extras" section, outside the series' count and status. The series request lists them in a separate table that starts unselected, and Full Series covers only the series' own books.Book covers reverted to full size every few hours
[fix/image-proxy-stale-optimize]— refreshing an expired image-cache entry wrote the original download back over the optimized copy and never optimized it again. Every cover went back to full size once its cache lifetime ran out, which is 4 hours for Hardcover's CDN. A refresh now optimizes the new download too.The first view of each cover sent the full-size original
[fix/image-proxy-first-request-optimize]— on a cache miss the image proxy returned the original download and optimized it in the background. The first person to view any cover therefore received the full-size file, and their browser kept it for the cache lifetime. A page of 40 covers nobody had viewed came to 10.5 MB, against 2.5 MB once optimized; one cover was a 552 KB JPEG as downloaded and a 54 KB WebP after. The proxy now waits for the transform and serves the original only if the transform fails. Only the Hardcover proxy has a transform, so TMDB, TVDB and avatar images are unaffected. Adds the first tests for the image proxy.Features
Per-format status on book title cards
[feature/audiobook-tile-status]— book tiles previously showed ebook status only. They now show one chip per format, each styled like the existing badge (same fill, border weight, and check glyph) with a book or speaker icon marking the format: green check when available, green minus when that format is available but the other is not, red X when Readarr monitors the book but has no file and no seerr request exists, and the existing clock while requested and searching. The audiobook chip is gated onbookAudioEnabledplusMANAGE_REQUESTS/REQUEST_4K/REQUEST_AUDIO_BOOK. Movie and series badges are unchanged. On/book/{id}, badges now read "Ebook Available" / "Audiobook Available", and the available format shows "Partially Available" while the other is missing. This requiredgetRelatedMediato joinmedia.requestsso list payloads can distinguish "requested" from "merely monitored".Sort and filters on book discovery
[feature/book-discover-filters]— the Books discover page gains the sort dropdown and filter slide-over that movies and series already have, limited to filters Hardcover can back: release-date range and genre multi-select. With nothing applied the page returns the trending list exactly as before; any filter or non-default sort switches to a filtered catalog query. Two Hardcover API notes for anyone extending this: the endpoint rejectsbooks_bool_expandbooks_order_bypassed as GraphQL variables ("not a valid graphql query"), so the where clause and ordering are built inline with strict sanitization (genre slugs matched against^[a-z0-9-]+$, dates againstYYYY-MM-DD, sort keys from a fixed map); and sorting by newest clamps to one year out, because the catalog contains corrupted far-future release dates (year 20222).One action can cover both book formats
[feature/sync-book-format-requests]— adds asyncBookFormatRequestsmain setting, surfaced as "Request Both Book Formats by Default", which pre-selects both formats when requesting a book. It is disabled unless a default Audiobook Readarr server is configured, matching the existing audiobook permission gating. Because it is global rather than a permission, it applies to users holding only one ofREQUEST_BOOK/REQUEST_AUDIO_BOOK: only permitted formats are submitted, and lacking the other bit never fails the request. The book quota counts distinct books rather than request rows, so taking a book in both formats costs one unit, not two.Format picker for book requests
[feature/book-format-request-picker]— requesting a book previously decided its formats behind a single button, and the audiobook was reachable only from a dropdown on the detail page. The request modal now lists every format the user may request with its own toggle and status, in the same shape as the existing season picker, with formats that are already available or requested shown locked. Book detail pages open that same modal from one Request button rather than a dropdown. Advanced options render one panel per selected format, each bound to its own Readarr instance, so requesting both formats no longer applies one instance's server, profile and root folder to the other. Title cards show their request button whenever any format is still open, so an available ebook no longer hides its audiobook, and they track both formats after a request completes. Formats are submitted independently, so one that is already requested cannot sink the other.Per-book format picker for series requests
[feature/series-format-request-picker]— a series request covered one format at a time, chosen from a dropdown, so a series wanted partly as ebooks and partly as audiobooks took two passes. The modal now opens with a Full Series table in the same shape as the single-book picker: a toggle for the whole series, then one row per format with its toggle and the series' status in that format. Below it, each book has a request toggle followed by its own ebook and audiobook toggles and their status. A book follows the series-level formats until its own are changed. Switching a book, or the whole series, off disables its format toggles but keeps their status visible, and books already held show as on and cannot be toggled. Each book is submitted only in the formats selected for it, so a format already held cannot fail the batch, and a book still costs one unit against the book quota however many of its formats are taken. Series requests can also be turned off entirely from Settings, which hides the request button while leaving series browsable.Housekeeping
pnpm i18n:extracthad not been run against the book work, soen.jsonwas missing keys from this fork's existing components (BookDetails, HardcoverSetup, SearchInput, Sidebar, book permissions) as well as the ones added here. The extract commit adds them and picks up two string updates already made in the source ("Request 4K" → "Request 4K/Audiobooks"). Two merge-resolution files also neededpnpm format.How Has This Been Tested?
Running in production on a home media server: Docker image built from this branch, Jellyfin, and Chaptarr configured as two Readarr instances (ebook and audiobook) addressed via URL bases, backed by a Calibre library.
Repository checks, all run against this branch:
pnpm build— passes (the image build runs it)pnpm test— 242/242 passing, 0 failurespnpm typecheck— cleanpnpm lint— 0 errors (24 pre-existing warnings)pnpm format:check— cleanpnpm i18n:extract— run, output committed; running it again changes nothingFunctional testing:
media_requestrows, one per format, which remain independently approvable and fulfillable.REQUEST_BOOKis offered only the ebook format, a user with onlyREQUEST_AUDIO_BOOKonly the audiobook, a user with neither gets no request button, and a hand-made request for an unheld format is rejected by the server.Screenshots / Logs (if applicable)
Global Discovery Page:










Discovery page with added filters and status chips:
Discovery page filtering:
Discovery Book Request Form:
Book page with availability chips:
Book page request form:
Series page with availability chips, per book cover chips and separation of extras:
Series page request form:
Settings:
More Settings:
Checklist:
pnpm buildpnpm i18n:extract