Skip to content

fix(import): actually embed the ASIN after an import - #843

Open
m4bard wants to merge 1 commit into
Listenarrs:canaryfrom
m4bard:fix/bug11-taglib-writestream
Open

fix(import): actually embed the ASIN after an import#843
m4bard wants to merge 1 commit into
Listenarrs:canaryfrom
m4bard:fix/bug11-taglib-writestream

Conversation

@m4bard

@m4bard m4bard commented Aug 20, 2026

Copy link
Copy Markdown

Summary

After a successful manual import, the step that writes the book's ASIN into the imported file's own tags never gets there. Two independent faults, and the import reports success through both of them, because the enrichment step is deliberately non-fatal.

Full write-up, measurements and the one decision I did not want to make myself are in #842.

Changes

Fixed

  • UnixOpenFlags gains OpenReadWriteNoFollow() beside the existing OpenWriteNoFollow(). PinnedFileEntry.OpenIndependentWriteStream becomes OpenIndependentReadWriteStream and uses it, so the stream handed to TagLib can be read as well as written. Mpeg4.File.Save() parses the existing box structure through that same stream before writing, so a write-only handle throws NotSupportedException mid-parse.
  • ApplyAsinTag asks for the tag by type, file.GetTag(TagTypes.Apple, create: true), rather than testing file.Tag is AppleTag. An MPEG-4 file's Tag is a CombinedTag wrapping the Apple tag, so the type test never matched and Save() rewrote an unchanged file. Only MPEG-4 answers to Apple, so mp3 and flac fall through as before.

The second fault is why the first one alone is not enough. On a build carrying only the stream change, the writer stops throwing and logs Wrote ASIN tag at Debug for a file whose bytes did not change. That was observed on the way through rather than predicted.

Pinning is unchanged. The handle is still opened relative to the pinned parent, still refuses to follow a link, and is still checked against the validated file object. Widening the access mode does not widen what the lease will open.

Testing

PinnedAudiobookFileRegistrationLeaseTests gains a case asserting the metadata write stream is readable, seekable and returns the file's existing bytes. Verified as a real guard in both directions: with FileAccess.Write restored it fails on CanRead, and with the change it passes.

UnixOpenFlagsTests gains a case for the new method, asserting that only the access-mode bits differ from OpenWriteNoFollow and that everything else is identical. It is written as a difference rather than as a literal on purpose, since #828 established that noFollow is not the same value on arm64 and x64.

Full suite on this branch: 3,031 passed, 0 failed, 125 skipped, against a 3,029 baseline on 03958c15.

Reproduced end to end before and after against ghcr.io/listenarrs/listenarr:canary. On canary the imported file reads untagged with one Failed to write ASIN tag in the log; on a build of this branch it reads tagged and ffprobe reports the ASIN on the destination. The check is public, in the test-data repo linked from the issue, and it refuses a verdict unless a hand-stamped control reads tagged and the input file reads untagged in the same run.

Notes

This is rebuilt on top of #828. The earlier version of the change added a bool readable parameter to GetUnixWriteExistingFlags, which that commit deleted. Adding a sibling method to the new helper is both smaller and better, since it picks up the per-architecture noFollow detection rather than carrying a literal of its own.

One thing I would rather you decided than me, and it is the reason this is worth a look beyond the diff. The lease exists to fence a specific generation of a specific file, and I do not know whether write-only was a deliberate part of that. Nothing reads back through the handle today and the pinning is enforced separately, which is why I think widening it is safe. If it was deliberate, the fix belongs elsewhere: open a separate read+write handle for tagging and verify identity against the lease, rather than widening the lease's own. Say so and I will redo it that way.

Two independent faults stop the post-import ASIN write from reaching the file. The
import reports success either way, because the enrichment step is deliberately
non-fatal, so neither is visible without reading the destination file's tags.

The stream is write-only. TagLibAudioTagWriter's file abstraction hands TagLib the
lease's metadata write stream, which reaches OpenIndependentWriteStream and opens
O_WRONLY on Unix, or NtCreateFile without GenericRead on Windows. Mpeg4.File.Save()
parses the existing box headers through that same stream before it writes, so it
throws NotSupportedException mid-parse. Open read+write instead, via a new
UnixOpenFlags.OpenReadWriteNoFollow() alongside the existing OpenWriteNoFollow().

The tag lookup never matches on MPEG-4. ApplyAsinTag tests `file.Tag is AppleTag`,
but an MPEG-4 file's Tag is a CombinedTag wrapping the Apple tag, so the branch is
never taken. The Id3v2 and Xiph branches do not match an m4b either, so Save()
rewrites an unchanged file and the writer logs success. Ask for the tag by type
instead. Only MPEG-4 answers to Apple, so mp3 and flac fall through as before.

The second fault matters for how the first is judged: fixing only the stream turns
a logged failure into a silent one. That was observed on a build carrying just the
stream change, not predicted.

Pinning is unaffected. The handle is still opened relative to the pinned parent,
still refuses to follow a link, and is still checked against the validated file
object. Widening the access mode does not widen what the lease will open.

Rebuilt on top of Listenarrs#828, which replaced the hardcoded open-flag constants with
UnixOpenFlags. The new method inherits that commit's per-architecture noFollow
detection rather than reintroducing a literal.
@m4bard
m4bard requested a review from a team August 20, 2026 16:44
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