Fixes #962.
Two commits, backend only.
The first makes the author and series monitoring jobs keep what the catalog gives them. Both mappers took book.Series?.FirstOrDefault() and wrote its name and position into the legacy Series and SeriesNumber fields, never setting SeriesMemberships, so every book that arrived through monitoring got one membership built from the legacy fallback with no identifier, and any further series was discarded. They now build the full list through the same MetadataConverters.BuildSeriesMemberships the search conversion uses, and the legacy fields are read off the primary membership after normalization so the two cannot disagree. BuildSeriesMemberships goes from private to internal for that; same assembly.
The second gives AudibleBookMetadata a SeriesAsin property and passes it as the legacy identifier at the four places that already pass Series and SeriesNumber (ToAudiobook, the add workflow, the rescan patch, the naming preview). The frontend has declared and sent seriesAsin all along; it never bound. A payload that only carries the legacy fields can now keep its identifier. The rescan guard is deliberately not widened to a payload carrying only an identifier, because the helper's fallback needs a series name and an identifier alone would wipe the book's existing memberships.
Tests: six on the mappers (two series with identifiers kept in order with the first primary, legacy fields equal to the primary, no series, blank identifier), one on the metadata model, and the existing suites unchanged. dotnet format --verify-no-changes clean.
Validated on a running install, but for the wizard add path only. Importing a fixture book through the library import wizard produced one new membership with the series identifier from the catalogue lookup on it, and its name and position matched the legacy fields. The control is the rest of the membership table: a couple of thousand pre-existing rows, none of them w...
Automated Canary build