Skip to content

fix(scan): normalize paths when deduplicating audiobook file rows#662

Open
kevinheneveld wants to merge 1 commit into
Listenarrs:canaryfrom
kevinheneveld:fix/audiobookfile-path-dedup-normalization
Open

fix(scan): normalize paths when deduplicating audiobook file rows#662
kevinheneveld wants to merge 1 commit into
Listenarrs:canaryfrom
kevinheneveld:fix/audiobookfile-path-dedup-normalization

Conversation

@kevinheneveld

Copy link
Copy Markdown
Contributor

Problem

EfAudiobookFileRepository.ExistsAtPathAsync deduplicates stored file rows by raw exact-string match (f.Path == path). A row left by an older scan under a bare/relative path (e.g. Elantris.mp3) never string-matches the absolute path a current scan resolves for the same physical file, so the scan inserts a second absolute-path row.

The result is two AudiobookFile rows for one file:

  • the absolute row streams fine;
  • the bare row resolves relative to the API process's working directory, fails the stream endpoint's root-folder guard, and 404s on playback.

The detail page masks this because it reconstructs a display path from BasePath, so the two rows look identical to the user.

Fix

Keep the exact match as a fast path. Only when it misses, resolve the book's existing rows to a canonical absolute form — anchoring relative paths on the audiobook's BasePath via FileUtils.CombineWithOptionalBase + NormalizeStoredPath — and compare. The same file is then recognised regardless of stored path shape (legacy relative vs. absolute, separator/.. differences), so a rescan no longer spawns a duplicate.

A book has only a handful of file rows, so the fallback stays cheap and never runs for the common all-absolute case. IsPathUsedByOtherAsync (the cross-book guard) is left as an exact match by design — a file lives under exactly one book's folder, and resolving every other book's relative paths would require each book's BasePath.

Tests

Adds EfAudiobookFileRepository_ExistsAtPathTests covering exact match, the bare-relative-vs-absolute regression, a non-matching path, and the empty-rows case.

🤖 Generated with Claude Code

ExistsAtPathAsync matched stored file rows by raw exact string, so a row
left by an older scan under a bare/relative path (e.g. "Elantris.mp3")
never matched the absolute path a current scan resolves for the same
file. The scan then inserted a duplicate absolute-path row; the bare row
resolved relative to the process working directory and 404'd on playback.

Keep the exact match as a fast path and, only on a miss, resolve the
book's rows to a canonical absolute form (anchoring relative paths on the
audiobook BasePath) before comparing. A book has few file rows, so the
fallback is cheap and never runs for the common all-absolute case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kevinheneveld kevinheneveld requested a review from a team June 9, 2026 00:40
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