fix(scanner): re-extraction is write-after-success — no NULL audio_metadata

The 2026-10-21 wipe: a re-extraction pass NULLed audio_metadata on the
m4b audiobooks — every affected book dropped its chapters, track
manifest and duration and displayed as 1 second long (the reader also
lost its manifest, since single-file books synthesize it from the
payload).

Guards on both writers of that column:
- updateMediaItemWithMetadata (generic full-row pass): when the new
  extraction carries no audio payload, keep existing.AudioMetadata.
  Single-file m4b books route here, and a failed/absent extraction
  leaves AudioMeta nil.
- updateAudioBookItem (focused payload write): an empty payload (no
  tracks, no chapters, no duration) over a populated row is a failed
  extraction — refuse the write with a loud warning and let the next
  scan retry.

audioPayloadIsEmpty() pins the empty-payload predicate (unit-tested):
the column is only ever replaced by fresh data from a real audio
extraction, never by absence of one.
This commit is contained in:
John O'Keefe committed 2026-10-21 20:22:57 -04:00
1 parent 970beee4b8
commit 80bfc1a77b
3 files changed
+64 -1

No files matched your search

+13 -1
View File
@@ -4049,6 +4049,18 @@ func (s *MediaScanner) updateMediaItemWithMetadata(ctx context.Context, existing
if metadata.AlternateInfo != "" {
alternateInfoBytes = []byte(metadata.AlternateInfo)
}
// A re-extraction that produced no audio payload must never wipe
// scan-derived audio_metadata (tracks/chapters/duration): the
// ev-gated generic pass runs for multi-file books too, where
// extractMetadata leaves AudioMeta nil — writing NULL here deleted
// every folder book's payload and broke playback (2026-10-21
// incident: all multi-file audiobooks showed as 1 second long). The
// audio chain (UpdateMediaItemAudioBookPayload) owns this column;
// without fresh data from THAT chain, keep what the row has.
audioMetadata := marshalAudioMetadata(metadata.AudioMeta)
if audioMetadata == nil {
audioMetadata = existing.AudioMetadata
}
params := database.UpdateMediaItemParams{
ID: existing.ID,
Title: metadata.Title,
@@ -4089,7 +4101,7 @@ func (s *MediaScanner) updateMediaItemWithMetadata(ctx context.Context, existing
AlternateInfo: alternateInfoBytes,
ScanInformation: pgtype.Text{String: metadata.ScanInformation, Valid: metadata.ScanInformation != ""},
Summary: pgtype.Text{String: metadata.Summary, Valid: metadata.Summary != ""},
AudioMetadata: marshalAudioMetadata(metadata.AudioMeta),
AudioMetadata: audioMetadata,
}
// Keep user-customized fields, and keep the override set itself intact.