Skip to content

fix(AudioTag): null-safe Diff() for OriginalReleaseDate - #161

Merged
pennydreadful merged 2 commits into
pennydreadful:developfrom
csmarshall:fix/audiotag-diff-null-release-date
Sep 10, 2026
Merged

pennydreadful merged 2 commits into
pennydreadful:developfrom
csmarshall:fix/audiotag-diff-null-release-date

Conversation

@csmarshall

@csmarshall csmarshall commented May 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

AudioTag.Diff() throws System.InvalidOperationException: Nullable object must have a value when the file's OriginalReleaseDate is set but the metadata being compared against has it null (or vice versa). Three-line fix: only apply the year-only shortcut when both sides have a value.

Context

Hit consistently after migrating an existing Readarr/Goodreads install to the hardcover image — Hardcover metadata commonly returns null release dates for works where Goodreads filled them in. Stack trace from a live failure:

[v0.4.20.129] System.InvalidOperationException: Nullable object must have a value.
   at NzbDrone.Core.MediaFiles.AudioTag.Diff(AudioTag other) in ./Readarr.Core/MediaFiles/AudioTag.cs:line 493
   at NzbDrone.Core.MediaFiles.AudioTagService.WriteTags(BookFile trackfile, Boolean newDownload, Boolean force) in ./Readarr.Core/MediaFiles/AudioTagService.cs:line 162
   at NzbDrone.Core.MediaFiles.BookImport.ImportApprovedBooks.Import(List`1 decisions, ...) in ./Readarr.Core/MediaFiles/BookImport/ImportApprovedBooks.cs:line 253

Downstream effect: ImportApprovedBooks bails on the file's import when the tag-write step throws, so no BookFile row gets written. In my install I saw the import scan run with writeaudiotags=Sync log a handful of explicit Couldn't import book warnings citing this stack trace, and ended with BookFiles = 0 after metadata refresh + identification had completed for ~14k files. Setting writeaudiotags=No is the workaround that unblocked the import (BookFiles populated to ~13.9k cleanly); this PR is the proper fix.

Root cause

The outer ?.Date != other.?.Date check correctly passes when one side is null. The inner ID3v2.3 year-only shortcut then dereferences other.OriginalReleaseDate.Value.Year unconditionally, crashing when other is null.

The companion Date diff a few lines above is already null-safe (uses HasValue before .Value); this branch is just an oversight.

Fix

                 // Id3v2.3 tags can only store the year, not the full date
                 if (OriginalReleaseDate.HasValue &&
                     OriginalReleaseDate.Value.Month == 1 &&
-                    OriginalReleaseDate.Value.Day == 1)
+                    OriginalReleaseDate.Value.Day == 1 &&
+                    other.OriginalReleaseDate.HasValue)
                 {

When other is null, fall through to the existing else branch which already handles HasValue on either side and emits a clean diff entry.

Verification

  • Crash signature reproduced in my install on the current hardcover-v0.4.20.129 image (logs above).
  • Code review: the fix matches the null-safety pattern already in use for the sibling Date diff a few lines above.
  • Built locally: dotnet build NzbDrone.Core/Readarr.Core.csproj -c Release clean — 0 warnings, 0 errors.
  • CI to verify no regressions in the existing test suite.

No existing unit test covers AudioTag.Diff() in this repo (checked under src/ for any *AudioTag*Test* files — none). Glad to add one if wanted; didn't want to scope-creep without a signal.

AudioTag.Diff() throws "Nullable object must have a value" when
comparing a tag whose file has a populated OriginalReleaseDate against
metadata where the corresponding field is null (e.g. Hardcover metadata
that omits the release date for a work).

The outer `?.Date != other.?.Date` check correctly passes when one side
is null. But the year-only shortcut (added for ID3v2.3 which can only
store a year, not the full date) then dereferences
`other.OriginalReleaseDate.Value.Year` unconditionally, crashing when
other is null. The downstream effect is ImportApprovedBooks bailing on
the entire file import - even though tag writing isn't strictly required
for the import to succeed - so no BookFile row is written for that file.

Apply the year-only shortcut only when both sides have a value;
otherwise fall through to the full-date else branch which already
handles a null on either side. The companion Date diff a few lines
above is already null-safe in this way.

Repros consistently when migrating an existing Goodreads-backed install
to the hardcover image: 14000+ existing audiobook files all fail to
import via tag-write failure because Hardcover commonly returns null
release dates where Goodreads filled them in.
axsddlr added a commit to axsddlr/bookshelf that referenced this pull request Aug 1, 2026
- Sanitize DB connection string in MigrationController logs (pennydreadful#101)
- Accept empty body as qBittorrent auth success (HTTP 204, pennydreadful#178)
- Add qBittorrent API key auth support from Radarr/Sonarr
- Null-safe Diff for OriginalReleaseDate in AudioTag (pennydreadful#161)
- NRE fix: null guards for series/LinkItems in MetadataProfileService (pennydreadful#63)
- Materialize ListIds to array before GraphQL serialization (pennydreadful#168)
- Robust file enumeration with per-file error handling in DiskScanService (pennydreadful#138)
- Only delete same-edition files on import upgrade (pennydreadful#173)
- Replace Goodreads ID with generic Book ID in search UI (pennydreadful#88)
@pennydreadful
pennydreadful enabled auto-merge (squash) September 10, 2026 18:24
@pennydreadful
pennydreadful merged commit 0ee3171 into pennydreadful:develop Sep 10, 2026
1 check passed
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.

2 participants