Skip to content

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

Open
m4bard wants to merge 2 commits 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 2 commits 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.

m4bard and others added 2 commits September 10, 2026 13:13
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.
…ct and fix the unmatched-files mapping

Three callers were each doing their own version of the same translation, so
they disagreed about what a series is. The walk now lives once in
buildSeriesFields, which returns the memberships and the legacy series,
seriesNumber and seriesAsin taken off the primary membership, so those can no
longer drift apart. matchToMetadata uses it in place of its inline copy, and
the ASIN sanity check it needs is now a named looksLikeAsin the query parser
uses as well.

Two behaviours change on purpose in the course of that: the legacy scalars are
read off the first entry that actually has a name rather than off entry zero
whatever it holds, and seriesAsin is checked for an ASIN shape before it
reaches the scalar, where until now the membership dropped a bogus value while
the scalar beside it kept one.

_enrichMetadata re-fetches the full product record before the add and was
lifting only authors and narrators out of it, leaving the series data at
whatever the search match happened to know. It now takes the memberships and
the matching legacy fields from that record when it has any, since the product
lookup is the better source of both.

The unmatched-files bulk add read series[0].title and .part off a response
whose entries carry asin, name and position. Those field names are Audnexus
names on an Audible-shaped payload, so the read never matched and every add
fell back to the series string parsed out of the file tags. It reads the right
fields now, keeps every membership and carries the identifier. mapToAudible is
exported so the mapping is tested directly instead of through the bulk-add
flow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDr4ZwWYwhX8fAy26Vq5KD
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