Skip to content

fix(library-import): keep every series membership, not just the first - #847

Open
m4bard wants to merge 1 commit into
Listenarrs:canaryfrom
m4bard:fix/bug5-library-import-series-memberships
Open

fix(library-import): keep every series membership, not just the first#847
m4bard wants to merge 1 commit into
Listenarrs:canaryfrom
m4bard:fix/bug5-library-import-series-memberships

Conversation

@m4bard

@m4bard m4bard commented Aug 20, 2026

Copy link
Copy Markdown

Summary

Library Import collapses a multi-series match to its first series and never populates seriesMemberships, so the backend falls back to the legacy scalar path and stores one membership. This builds the membership list in the store instead.

Full write-up and the hazard I had to guard against are in #846.

Changes

Fixed

  • matchToMetadata in fe/src/stores/libraryImport.ts maps every entry of the search result's series array into an AudiobookSeriesMembership, with isPrimary and sortOrder following the incoming order, and sets seriesMemberships on the metadata when there is at least one.

The scalar series, seriesNumber and seriesAsin fields stay populated from the first entry, so nothing reading them changes behaviour. AudiobookSeriesMembershipHelper.Normalize only synthesizes from the scalar when no memberships arrive, so sending the list is enough to take the other path.

Guarded

  • A membership's seriesAsin is kept only when it matches /^[A-Z0-9]{10}$/i.

This is not cosmetic. SearchResponseMapper's fallback branch, used when the ASIN re-fetch fails, sets a synthesized entry's asin to the series name. That is harmless today because the value has nowhere to land and is dropped on the way in. Sending memberships gives it somewhere to land, and it would be persisted into AudiobookSeriesMembership.SeriesAsin, the column #767 is working to make trustworthy. Making the mapper emit null would be the better fix and belongs in its own change; this keeps the bad value out in the meantime.

The second flattening at the add call site is deliberately untouched. It adapts to SearchResult.Series, which is string? on the backend, so it is a single-series contract rather than a second instance of the bug.

Testing

Two cases added to fe/src/__tests__/libraryImport.store.spec.ts: that every membership of a multi-series match reaches the add request, and that a series asin which is really the series name is dropped.

Verified as real guards in both directions. With only the source change reverted and both tests kept, both fail and the other seven in the file pass. Restored, all nine pass.

Full frontend suite: 597 tests across 89 files, all passing. vue-tsc type-check clean, prettier and eslint clean on both changed files.

Notes

No container reproduction for this one, and I would rather say so than imply parity with the other reports. This is browser-side logic on the path between a search result and the add request; no scan, import action or database state exercises it, so a harness check here would not be testing the thing that is broken.

The multi-series case is real rather than constructed: B00CQ5WAXW returns seriesPrimary "Ayesha" and seriesSecondary "Allan Quatermain" from api.audnex.us, verified live rather than taken from a quote.

matchToMetadata collapsed a search result's series array to its first entry and
sent only the legacy scalar series/seriesNumber. AudibleBookMetadata already
carries SeriesMemberships, and /library/add applies it via
AudiobookSeriesMembershipHelper.ApplyToAudiobook, falling back to the single
scalar only when no memberships are supplied. So a book in more than one series
lost every membership after the first on this path, while the Add New path
(AddLibraryModal) preserved them.

Build one membership per series entry, ordered, with the first marked primary.
The scalar fields stay populated from the primary so nothing downstream changes.

The search endpoint's fallback branch fills a series entry's asin with the
series name when it cannot re-fetch the book by ASIN. That value used to be
discarded because AudibleBookMetadata has no top-level SeriesAsin; a membership
would persist it, so only a value shaped like an ASIN is kept.
@m4bard
m4bard requested a review from a team August 20, 2026 17:36
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