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:
1 parent
970beee4b8
commit
80bfc1a77b
3 files changed
+64
-1
No files matched your search
@@ -1015,6 +1015,16 @@ func (s *MediaScanner) processAudioFolderBook(ctx context.Context, rootDir strin
|
||||
// rescan saw the track set change (or the admin forced a rescan). Only
|
||||
// scan-derived fields move — editorial columns keep admin edits.
|
||||
func (s *MediaScanner) updateAudioBookItem(ctx context.Context, existing database.MediaItems, payload *AudioMetadataPayload, mimeType string, totalSize int64, chapterCount int) error {
|
||||
// Write-after-success discipline (2026-10-21 incident: multi-file
|
||||
// audiobooks surfaced as 1 second long because a re-extraction wrote
|
||||
// a NULL/empty audio_metadata over a populated one). An empty payload
|
||||
// is a FAILED extraction, not a book with no audio: when the row
|
||||
// already carries audio data, keep it and skip the write — the next
|
||||
// scan retries, and the reader keeps its manifest in the meantime.
|
||||
if audioPayloadIsEmpty(payload) && existing.AudioMetadata != nil {
|
||||
fmt.Printf("[AUDIO] WARNING: re-extraction of '%s' produced an empty audio payload; keeping the existing audio_metadata\n", existing.Title)
|
||||
return nil
|
||||
}
|
||||
if _, err := s.db.UpdateMediaItemAudioBookPayload(ctx, database.UpdateMediaItemAudioBookPayloadParams{
|
||||
ID: existing.ID,
|
||||
FileSize: pgtype.Int8{Int64: totalSize, Valid: true},
|
||||
@@ -1028,6 +1038,14 @@ func (s *MediaScanner) updateAudioBookItem(ctx context.Context, existing databas
|
||||
return nil
|
||||
}
|
||||
|
||||
// audioPayloadIsEmpty reports whether a payload carries no audio facts
|
||||
// at all — no tracks, no chapters, no duration. Such a payload is the
|
||||
// signature of a failed/absent extraction, never of a real book.
|
||||
func audioPayloadIsEmpty(payload *AudioMetadataPayload) bool {
|
||||
return payload == nil ||
|
||||
(len(payload.Tracks) == 0 && len(payload.Chapters) == 0 && payload.DurationSeconds <= 0)
|
||||
}
|
||||
|
||||
// multifileSidecarChanged runs the manifest ladder for a folder/zip book
|
||||
// (faceted plan Phase 6): a size-stable multi-file book is changed by
|
||||
// EDITING its sidecars — the track-count/size fingerprint never saw it,
|
||||
|
||||
Reference in new issue
Block a user