Skip to content

fix(scan): match audiobook files by embedded tags when path heuristics fail#688

Open
rknall wants to merge 1 commit into
Listenarrs:canaryfrom
rknall:fix/scan-embedded-tag-matching
Open

fix(scan): match audiobook files by embedded tags when path heuristics fail#688
rknall wants to merge 1 commit into
Listenarrs:canaryfrom
rknall:fix/scan-embedded-tag-matching

Conversation

@rknall

@rknall rknall commented Jun 15, 2026

Copy link
Copy Markdown

Summary

Audiobooks whose on-disk layout does not encode the title/author in the folder or filename — e.g. AudioBookShelf-style libraries with series-creator folders and numbered episode filenames — were left with zero linked files after a scan, even when the files sat in the correct BasePath. The per-audiobook scan attributed files purely by path/name heuristics (filename/folder must contain the title, or the path must contain the author), which reject these files.

This adds an embedded-tag confirmation fallback: for candidates the path heuristics reject, the scan reads the file's embedded ID3/MP4 tags (via the bundled ffprobe, reusing PathMetadataParser) and links a file when its embedded ASIN matches the audiobook (definitive), or when both title and author agree after normalization.

Changes

Added

  • ScanBackgroundService.MatchEmbeddedTags — a pure, unit-tested decision for attributing a file to an audiobook from its embedded tags (ASIN = definitive; title+author = softer fallback).
  • Embedded-tag confirmation fallback in the per-audiobook scan, reusing PathMetadataParser.ReadEmbeddedTagsAsync + the bundled ffprobe.
  • Unit tests covering the match logic.

Changed

  • Scan tag reads run concurrently with bounded parallelism (min(4, CPUs)); the match decision is applied on a single thread.

Testing

  • New unit tests ScanBackgroundServiceTagMatchTests (9 cases: ASIN match incl. case/whitespace insensitivity, title+author when the ASIN differs, punctuation/subtitle tolerance, author-only and title-only negatives, null/empty tags) — all pass.
  • Manual: a real audiobook filed under a series-creator folder with a numbered filename (.../Elfie Donnelly/Bibi und Tina/61 - Retten die Biber/Bibi und Tina - 61 - Retten die Biber.mp3) went from 0 → 1 linked file, confirmed via the embedded ASIN.

Notes

  • The fallback only runs for candidates the existing path heuristics reject, so the common already-matching path is unchanged (no added ffprobe cost there).
  • Follow-ups intentionally out of scope: teaching PathMetadataParser to also read plain title/artist tags (it currently reads album/album_artist); and regional ASIN mismatches (file tag carries the .de ASIN while the record holds the .com ASIN), which belong with the regional-search work.

…tics fail

The per-audiobook file scan attributed candidate files using only path/name
heuristics: a file was kept only if its filename or folder contained the
audiobook title, or its path contained the author. Layouts where the
folder/filename does not carry that information (e.g. AudioBookShelf-style
series-creator folders with numbered episode filenames) had every candidate
rejected, so correctly-placed files were never linked (0 files imported).

Add an embedded-tag confirmation fallback: for candidates the path heuristics
reject, read the file's ID3/MP4 tags (reusing PathMetadataParser via the
bundled ffprobe) and attribute the file when the embedded ASIN matches the
audiobook (definitive), or when both title and author agree after
normalization. Tags are read concurrently with a bounded degree of
parallelism; the match decision is applied on a single thread.

The match logic is factored into ScanBackgroundService.MatchEmbeddedTags and
covered by unit tests.
@rknall
rknall requested a review from a team June 15, 2026 08:11
@therobbiedavis therobbiedavis added the patch patch version bump - backward compatible bug fixes label Jul 1, 2026
@m4bard

m4bard commented Jul 14, 2026

Copy link
Copy Markdown

The ASIN-definitive / title+author split here looks like the right shape to me, and I like that the fallback only runs on candidates the path heuristics already rejected, so the common case pays nothing.

Two notes, one of them just housekeeping.

This currently conflicts with canary. The scan service was reorganised after you opened this — ScanBackgroundService is now just the queue loop, and the per-audiobook path heuristics moved to ScanFileDiscovery.FindMatchingAudioFiles/Matches under listenarr.infrastructure/Library/Scanning/. It isn't a pure file move: FindMatchingAudioFiles is a sync static with no DI, so the tag fallback either needs it to go async and take IFfmpegService, or wants to run as a second pass in ScanJobProcessor, which already has the scope.

One thing I'd want to fix in the rebase rather than carry forward. Since the branch doesn't build against canary as-is, I lifted MatchEmbeddedTags and NormalizeTagToken out of the diff verbatim and ran them under xunit against a set of public-domain audiobooks with real Audible ASINs. Four of six cases come back wrong.

The title test is a bidirectional Contains over a normalized string, and NormalizeTagToken strips punctuation first — which removes the : that separates a subtitle from a different title. After normalization there's nothing left to tell them apart.

H. Rider Haggard is the clean example. She (1887), Ayesha: The Return of She (1905) and She and Allan (1921) are three distinct novels. Against the audiobook She:

tag title should match MatchEmbeddedTags returns
She: A History of Adventure yes — same work, full title TitleAndAuthor
She and Allan no — different novel TitleAndAuthor
Ayesha: The Return of She no — different novel TitleAndAuthor

They're all by Haggard, so authorMatch is true for all three. Within one author's folder tree the author half is true by construction, which leaves the title substring doing all the discriminating — and for a one-word title it discriminates almost nothing.

It isn't only short titles, either. Audible's real title for Haggard's Marie is Marie: An Episode in the Life of the Late Allan Quatermain, which contains Allan Quatermain verbatim — so a file tagged as Marie is attributed to the audiobook Allan Quatermain, with no normalization needed at all.

Worth saying: adding word boundaries doesn't fix this. I tried that first. she is a whole token in she and allan, and containment is deliberate — you need it for Title: Subtitle. What did work was cutting the subtitle before normalizing (split on the first : / / (, drop a trailing , Book N), then requiring exact equality when either title is under ~3 tokens and allowing token-boundary containment above that. That keeps your A Dance with Dragons: A Song of Ice and Fire, Book 5 case and the other decorated forms I threw at it, while rejecting all three Haggard collisions.

For context on why I care about the softer branch: across a test library of over 1,500 books, zero of the files carried an ASIN in any spelling ExtractAsin looks for — so for some libraries the definitive branch never fires and everything rests on title+author. And roughly 1 in 6 folders in that set have tags that contradict the folder, most often naming a different book by the same author. Your titleMatch && authorMatch requirement already rejects the plain version of that, which is why I think the approach is sound and worth getting over the line.

Are you planning to rebase this? Happy to help — I can send the title-matching fix and the xunit cases as a PR against your branch, or if it's easier, put up a fresh branch carrying your work forward with you credited. Whichever you prefer; I don't want to step on it. The test file drops in standalone either way, if it's useful.

(Unrelated to this PR, but for context on where I'm coming from: #763 is a naming-path fix I put up separately — series positions from Audnexus aren't always numbers, and a non-numeric one was being dropped and replaced by the track number. Different subsystem entirely; mentioning it only so the above doesn't read as a drive-by.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

patch patch version bump - backward compatible bug fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants