Files
bookhoard/internal/services/media_scanner_audio_guard_test.go
John O'Keefe 80bfc1a77b 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.
2026-10-21 20:22:57 -04:00

34 lines
1.3 KiB
Go

package services
import (
"testing"
)
// TestAudioPayloadIsEmpty pins the write-after-success guard on
// updateAudioBookItem (2026-10-21 incident: multi-file audiobooks
// surfaced as 1 second long because a failed re-extraction overwrote a
// populated audio_metadata). An empty payload — nil, or no tracks, no
// chapters, no duration — is the signature of a failed extraction and
// must never replace existing audio data.
func TestAudioPayloadIsEmpty(t *testing.T) {
require := func(cond bool, msg string) {
if !cond {
t.Fatal(msg)
}
}
require(audioPayloadIsEmpty(nil), "nil payload must read as empty")
require(audioPayloadIsEmpty(&AudioMetadataPayload{}), "zero-value payload must read as empty")
require(audioPayloadIsEmpty(&AudioMetadataPayload{DurationSeconds: 0}), "zero-duration payload must read as empty")
// A real book always carries at least one of the audio facts.
require(!audioPayloadIsEmpty(&AudioMetadataPayload{DurationSeconds: 302.6}),
"duration-only payload (single-track book) is not empty")
require(!audioPayloadIsEmpty(&AudioMetadataPayload{
Tracks: []AudioTrack{{Index: 0, Path: "01.mp3"}},
}), "track manifest is not empty")
require(!audioPayloadIsEmpty(&AudioMetadataPayload{
Chapters: []AudioChapter{{Index: 0, Title: "Chapter 1"}},
}), "chapters are not empty")
}