fix(scanner): nil metadata_overrides crashed Reset to Scanned with SQLSTATE 23502

Reset to Scanned cleared the overrides row (successfully) but then set
the in-memory copy to nil before handing it to updateMediaItem. pgx
encodes a nil []string parameter as SQL NULL, so the follow-up UPDATE
wrote metadata_overrides = NULL into the column's NOT NULL constraint
and the whole rescan failed with:

  failed to update media item: ERROR: null value in column
  "metadata_overrides" of relation "media_items" violates not-null
  constraint (SQLSTATE 23502)

Two changes:

- RescanMediaItem's reset path assigns []string{} instead of nil, with a
  comment explaining the pgx nil-to-NULL encoding trap.
- updateMediaItem routes the override set through utils.MergeOverrides,
  whose contract guarantees a non-nil slice, so no caller can write
  NULL into that column again (verified against pgx v5.9.2 source: a
  scanned '{}' round-trips as non-nil in both directions; the nil could
  only come from our own assignment).

The plain Rescan path never hit this - only Reset did. Worse, the reset
is the remedy when a book's cover_image_path override pins an empty
cover, so the crash also blocked the way out of that state. After this
fix, a plain rescan on an already-reset book repopulates scanned
metadata and extracts the cover.
This commit is contained in:
John O'Keefe
2026-09-13 00:12:56 -04:00
parent 1fa8ee3a59
commit 762087e0fd
+7 -2
View File
@@ -2847,8 +2847,10 @@ func (s *MediaScanner) updateMediaItem(ctx context.Context, existing database.Me
} }
// Keep user-customized fields, and keep the override set itself intact. // Keep user-customized fields, and keep the override set itself intact.
// MergeOverrides guarantees a non-nil slice: a nil []string would encode
// as SQL NULL and violate metadata_overrides' NOT NULL constraint.
utils.ApplyMetadataOverrides(&params, existing) utils.ApplyMetadataOverrides(&params, existing)
params.MetadataOverrides = existing.MetadataOverrides params.MetadataOverrides = utils.MergeOverrides(existing.MetadataOverrides)
_, err = s.db.UpdateMediaItem(ctx, params) _, err = s.db.UpdateMediaItem(ctx, params)
return err return err
@@ -2869,7 +2871,10 @@ func (s *MediaScanner) RescanMediaItem(ctx context.Context, mediaItemID pgtype.U
if err := s.db.ClearMediaItemMetadataOverrides(ctx, mediaItemID); err != nil { if err := s.db.ClearMediaItemMetadataOverrides(ctx, mediaItemID); err != nil {
return fmt.Errorf("failed to clear metadata overrides: %w", err) return fmt.Errorf("failed to clear metadata overrides: %w", err)
} }
item.MetadataOverrides = nil // Empty - not nil: pgx encodes a nil []string parameter as SQL NULL,
// which would violate the column's NOT NULL constraint when
// updateMediaItem writes the row back.
item.MetadataOverrides = []string{}
} }
folders, err := s.db.GetLibraryFolders(ctx, item.LibraryID) folders, err := s.db.GetLibraryFolders(ctx, item.LibraryID)