Skip to content

Track multiple narrations of the same audiobook - #58

Open
JordanFromIT wants to merge 1 commit into
Chaptarr:developfrom
JordanFromIT:feature/multiple-narrator-variants
Open

Track multiple narrations of the same audiobook#58
JordanFromIT wants to merge 1 commit into
Chaptarr:developfrom
JordanFromIT:feature/multiple-narrator-variants

Conversation

@JordanFromIT

@JordanFromIT JordanFromIT commented Aug 17, 2026

Copy link
Copy Markdown

Description

Some works have narrations worth keeping side by side — Jim Dale and Stephen Fry, Roy Dotrice and John Lee. Chaptarr already has most of what that needs: wanted instances carry BaseBookId, get a _wanted_{editionId} slug, de-duplicate against existing rows, and the {Narrator} naming token files them into separate folders. The narrator modal is already wired to addNarratorVariant, and it lists the narrators you don't yet have, which is additive by intent.

But you can only ever want one narration at a time.

AddWantedEdition creates a separate row only when the book already has files on disk. With no files it pins the chosen edition in place, so picking a second narrator overwrites the first. That means a variant can only be created for a book you already partly own — you cannot say "I want both of these" from a clean library, which is the normal starting point.

Observed on a book with 116 audiobook editions, selecting three narrators in turn:

Step before after
start 1 row — Simon Prebble 1 row — Simon Prebble
want Andrew Wincott 1 row — Wincott replaces Prebble 2 rows — Prebble + Wincott
want Stephen Fry 1 row — Fry replaces Wincott 3 rows — Prebble + Wincott + Fry
want Wincott again, via a different edition record 1 row — Wincott 3 rows — reuses the existing Wincott row

Before, every selection returns the same bookId and the previous choice is gone. After, the three narrations coexist and the repeat request de-duplicates.

What changed

AddWantedEdition takes an asNewVariant flag, exposed on the API as AsNewVariant. When set, the no-files branch is skipped so the narration is tracked alongside the current selection rather than replacing it. It defaults to false, so every existing caller keeps the current behaviour — there is a test pinning that.

De-duplication now also matches on narrator, not only on edition provider ids. Two printings of the same narration are distinct edition records but the same user request, so asking for a narrator already tracked returns the existing row instead of creating a near-duplicate. The base book is checked the same way, so no variant is created for a narration it is already pinned to. Names are compared on normalised tokens, because providers return inconsistent spacing — there are real records like "Peter Noble" and "Frank Muller".

The narrator modal now routes both cases through the endpoint. It previously reimplemented edition pinning client-side (GET /edition then PUT /book) for books with no files, which bypassed all of the above; deleting that branch removes the duplicated logic and is why the frontend diff is mostly red.

On the file count

30 files sounds worse than it is: 25 are one-line signature updates to IBookService test doubles. The real change is 3 source files plus tests.

Deliberately not included: Books.WantedNarratorId is never assigned anywhere in the codebase — only nulled by housekeeping and migrations — so variants remain keyed on the edition. Connecting the Narrators table to this flow looks like your call on the narrator entity's lifecycle rather than something to decide inside a bugfix-shaped change, so I left it alone. Happy to follow up if you want it.

This is independent of #56 and can merge in either order. They complement each other: #56 makes releases actually match the pinned narrator, this makes more than one narration trackable at once.

Database Migration

NO.

How was this tested?

Unit tests — 4 added to BookServiceWantedNarratorSeriesLinksFixture, each written first and observed failing for the right reason before implementing:

  • should_create_a_separate_variant_for_a_second_narrator_when_the_book_has_no_files
  • should_pin_in_place_without_creating_a_variant_when_not_requested — backward-compatibility guard
  • should_not_duplicate_the_base_book_when_it_already_wants_that_narrator
  • should_reuse_an_existing_variant_that_already_wants_the_same_narrator

The third caught a real duplicate-creation bug in my first implementation. Two unimplemented members on the fixture's in-memory doubles (InMemoryBookRepository.Update, InMemoryEditionService.UpdateMany) were filled in, since these paths now exercise them.

dotnet test src/Chaptarr.Core.Test/Chaptarr.Core.Test.csproj --configuration Release
develop (537eb64): 2834 passed, 0 failed
this branch:       2838 passed, 0 failed   (+4, the tests above)

Debug and Release both clean; dotnet build src/Chaptarr.NoTests.sln -c Release reports 0 warnings, 0 errors. The build.yml guard steps pass too — conflict markers, package.json parse, and version_guard.py in sync, monotonic and commit-hygiene modes. Frontend yarn typecheck passes and eslint is clean on the changed file (worth stating explicitly, since build.yml runs no yarn steps).

End-to-end, on Linux with .NET 10. Two containers from chaptarr/chaptarr:0.9.929, identical config databases, differing only by the patched assemblies, driven through POST /book/{id}/editions/wanted. That produced the table above — before, one row that keeps getting overwritten; after:

book 1:   slug='1984'            narrator='Simon Prebble'
book 647: slug='1984_wanted_7'   narrator='Andrew Wincott'
book 648: slug='1984_wanted_24'  narrator='Stephen Fry'

and the repeat request for Wincott through a different edition record returned 647 rather than creating a fourth row.

Screenshots (UI changes only)

No visual changes — the narrator modal looks and behaves the same; only the request it issues changed.

Selecting a narrator for a book with no files pinned that narration in
place, so choosing a second one silently replaced the first. Only one
narration could ever be wanted at a time, and the wanted-instance path
that creates a separate row was unreachable until the book already had
files on disk.

AddWantedEdition takes an asNewVariant flag, surfaced on the API as
AsNewVariant. When set, the no-files branch is skipped so the narration
is tracked alongside the current selection instead of replacing it. The
flag defaults to false, so existing callers keep the previous behaviour.

Wanted-instance de-duplication now also matches on narrator, not just on
edition provider ids. Two printings of the same narration are distinct
edition records but the same request, so asking for a narrator that is
already tracked returns the existing row. The base book is checked the
same way, so a variant is not created for a narration it is already
pinned to. Narrator names are compared on normalised tokens because
providers return inconsistent spacing such as "Peter   Noble".

The narrator modal now routes both cases through the same endpoint. It
previously reimplemented edition pinning client-side for books with no
files, which bypassed this logic entirely.

The test doubles for IBookService are updated for the new signature.
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