Read embedded ASIN/ISBN tags during scan (fixes Rescan Metadata for tagged files) - #781
Read embedded ASIN/ISBN tags during scan (fixes Rescan Metadata for tagged files)#781dny238 wants to merge 5 commits into
Conversation
…obook Files imported with an embedded ASIN (the freeform iTunes atom ----:com.apple.iTunes:ASIN that Audible rips and most audiobook taggers write) started life in the library with no identifier, so "Rescan Metadata" failed with "No ASIN or ISBN identifiers are available" even though the value was sitting in the file. - FfprobeTagMetadataMapper.Apply now reads the ASIN and ISBN tags into AudioMetadata (GetTag already matches case-insensitively). - AudiobookFileService.EnsureAudiobookFileAsync — the shared file- registration path used by the scan job — now adopts those identifiers onto the audiobook when it has none, persists, and records a history entry. Existing identifiers are never overwritten. Because AudiobookIdentifierMapper.GetEffectiveIdentifiers backfills from the legacy Asin/Isbn fields, this is enough for Rescan Metadata to work. - Adds FfprobeTagMetadataMapperTests covering ASIN/ISBN extraction, case-insensitivity, absent-tag, and no-overwrite. Fixes Listenarrs#780 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The initial change only adopted identifiers when a *new* AudiobookFile was created. Re-scanning a file that was already imported (e.g. after tagging it with an ASIN) returned early before the adoption ran, so the identifier was never picked up. Now, when a file is already registered and the audiobook still has no ASIN, the scan re-reads the file's tags and adopts an identifier. Guarded on a missing ASIN so the extra ffprobe read only happens when there's something to gain. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Previously, scanning a file with an embedded ASIN adopted the identifier but left the book otherwise blank — the user then had to know to click "Rescan Metadata" to actually see title/narrators/publisher/etc. Clicking Scan and seeing nothing change is confusing. Now, immediately after the scan adopts an ASIN onto a book that had none, the upstream metadata is fetched and any empty fields are filled in, so a single scan both discovers the identifier and populates the book. - New IAudiobookMetadataRefreshService (application) fetches Audible metadata by ASIN and fills only empty fields — existing/user-set values are never overwritten. Reuses IAudiobookMetadataService + MetadataConverters. - AudiobookFileService triggers it after adopting an identifier; failures are logged and never fail the scan. - Tests cover fill-empty-without-overwrite and the no-op case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
On #780 I said the MP4 key ought to be checked before anyone leaned on it, so I checked it rather than leave the hedge sitting there. Your assumption holds. Tagged files in each dialect, read with the command Listenarr runs (
The freeform atom does come through unprefixed, so There is one thing I'd think about before this lands, because it runs into a bug that's already live.
The adopt only fires when the audiobook has no ASIN, which is the library-import case this PR is aimed at, so the two conditions overlap rather than excluding each other. A wrongly linked file donates its ASIN, A cheap guard would be to require the linked files to agree: if the files linked to one audiobook carry more than one distinct ASIN, that disagreement is itself a signal the attribution is off, so adopt nothing instead of picking one. When attribution is right it changes nothing. Waiting until attribution is tightened would also do it, since #717 rewrites this area and wants a title match rather than accepting the author on its own. Two smaller things:
On the tests: they assert against hand-built JSON, so they'd pass unchanged even if ffprobe surfaced the tag under some other key, which is the one thing this PR depends on. The table above is that assumption actually checked. I have a corpus of tagged public-domain files covering every dialect if a fixture test would help, and I'm happy to hand over the files or the generator that makes them. |
…arrs#781) Per @m4bard's review: - ASIN adoption now requires agreement: it only adopts when every linked file that carries an ASIN carries the same one. If the linked files disagree, that's a signal the file-to-book attribution is wrong, so nothing is adopted rather than picking one and letting the metadata auto-refresh act on a wrong identifier (ResolveUnanimousFileAsinAsync). - ISBN is now adopted only when the book has none, mirroring ASIN. It no longer appends new values on top of an existing set, which was the route a wrongly linked file could take to accumulate a stray identifier. - The already-registered-file re-read now reuses the per-file/mtime metadata cache, so rescanning a book that has no embedded ASIN no longer re-runs ffprobe on every file each time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks for actually tagging files in each dialect and checking Pushed 4def14c addressing the three points:
One honest limitation I'd rather name than paper over: the agreement check sees the files linked at the moment adoption fires, and adoption fires on the first file that carries an ASIN, so a conflicting file linked later in the same scan can still slip through the window. Tightening that fully means moving adoption to a post-scan step once the file set is settled — happy to do that if you'd prefer it over the incremental guard, but it felt like more surface than this PR should carry given #717/#784 are removing the mis-attribution at the source. |
|
All three land, and the unanimity rule is a better shape than what I suggested. Refusing on disagreement rather than trying to pick a winner is the part that makes it safe. Readarr's answer to this is neither of the two options we were choosing between. It never adopts an identifier onto a book at all: it groups the files, identifies the group against candidate editions, and treats the embedded ASIN as one weighted input to that choice.
releases = _trackGroupingService.GroupTracks(localTracks);
foreach (var localRelease in releases) { ... IdentifyRelease(localRelease, ...); }
var asin = localTracks.MostCommon(x => x.FileTrackInfo.Asin);
if (asin.IsNotNullOrWhiteSpace() && edition.Asin.IsNotNullOrWhiteSpace())
dist.AddBool("asin", asin != edition.Asin);
else if (asin.IsNullOrWhiteSpace() != edition.Asin.IsNullOrWhiteSpace())
dist.AddBool("asin_missing", true);with the weights in Grouping before identification has been there since
The weights are worth a look either way. On the window you asked about. It is narrower than "later in the same scan", and it cuts in a direction worth naming. Adoption runs from That makes the two paths behave differently. Re-scanning a book that already has its files linked, your new None of which is a reason to hold the PR. Deciding once the file set has settled is what actually closes the window, but it is a different shape of change, and you are right that #784 and #717 remove the cause rather than the symptom. The one thing I would add now is a sentence on I can measure it instead of leaving it as a reading. The corpus has a deliberate wrong-ASIN tag state and the attribution harness can build a library where a scan links a file belonging to another book, so pointing those at each other would show whether a conflicting ASIN reaches adoption before the check sees it. Say the word and I will run it against On fixtures, yes, take whatever is useful. |
|
#717 changes AudiobookFile registration into a database-enforced filesystem identity/ownership contract and serializes path-bearing mutations through the audiobook operation boundary. The ASIN/ISBN tag extraction in this PR remains distinct, but please rebase on #717 and make identifier adoption run through the new registration/operation-lock flow so it cannot race file ownership or concurrent audiobook updates. |
Document that ResolveUnanimousFileAsinAsync only sees files linked at the moment it runs, so a lone early tagged file is trivially 'unanimous' and the guard is weakest on first-scan mis-attribution. Addresses @m4bard review feedback on Listenarrs#781. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@therobbiedavis sounds good — I'll rebase this on #717 once it lands and route the identifier adoption through the new registration/operation-lock flow so it can't race file ownership or concurrent audiobook updates. Happy to hold until #717 merges so I'm building against its final shape rather than a moving target. @m4bard thanks for the careful read. You're right that the guard is weakest on a first scan, where the first tagged file is "unanimous" just by being the only member — I've pushed a doc comment on |
|
Fixtures are ready. Six directories, each one book whose files are tagged individually:
Each set writes a Two things I checked rather than assumed. Every file was read back through ffprobe to confirm the identifier actually surfaces, because fixtures that silently carry nothing would turn a passing test into a test of nothing. And rebuilding replaces the set rather than merging into it, which matters because a stale file surviving into If the shape is not what you need, say what is missing and I will add cases. Adding one is a few lines. |
Fixes #780
Problem
When ListenArr scans an existing audio file that has an ASIN embedded in its tags (the freeform iTunes atom
----:com.apple.iTunes:ASINthat Audible rips and most audiobook taggers write), the ASIN is never extracted. The imported audiobook gets no identifier, and Rescan Metadata then fails immediately with "No ASIN or ISBN identifiers are available for metadata rescan" — even though the value is sitting in the file.Root cause:
FfprobeTagMetadataMapper.Apply()maps title/artist/album/track/disc/year but never reads the ASIN (or ISBN) tag.Fix
FfprobeTagMetadataMapper.Apply()now reads theASINandISBNtags intoAudioMetadata.AudioMetadataalready hadAsin/Isbnproperties, andGetTagalready matches case-insensitively (so the----:com.apple.iTunes:ASINatom, which ffprobe surfaces asASIN, is picked up).AudiobookFileService.EnsureAudiobookFileAsync()— the shared file-registration path the scan job uses — now adopts those identifiers onto the audiobook when it has none, persists via the audiobook repository, and records a history entry. Existing identifiers are never overwritten. SinceAudiobookIdentifierMapper.GetEffectiveIdentifiersbackfills from the legacyAsin/Isbnfields, setting them is sufficient for Rescan Metadata to succeed.Scope / notes
Tests
FfprobeTagMetadataMapperTests: ASIN + ISBN extraction, case-insensitive ASIN match, absent-tag leavesnull, and existing ASIN not overwritten.dotnet buildclean (0 warnings). Related suites (Files / Scanning / Ffmpeg / Metadata) — 174/174 pass.🤖 Generated with Claude Code