feat(schema): subtitle/explicit columns, authors+publishers arrays, float series numbers

Phase 1 of the series/collections/metadata overhaul
(~/.opencode/plan/series-collections-metadata-overhaul.md).

Schema (in place, idempotent):
- media_items.series_number INTEGER -> DOUBLE PRECISION: real-world
  ordinals are decimals (0.5, 1.5 novellas); INTEGER silently truncated
  them (calibre 2.5 pinned as 2 in tests).
- + subtitle TEXT, explicit BOOLEAN (sidecar ingest; ABS explicit /
  Audible isAdult).
- + authors/authors_search/publishers/publishers_search TEXT[] NOT NULL
  (narrators A2a precedent). Arrays are write truth; author/publisher
  varchars become trigger-regenerated display strings
  (sync_author_publisher_display) so every varchar consumer (trgm
  search, facets, API, OPDS/Kobo) keeps working unchanged.
- Row-level NULL normalization for the new arrays in
  set_library_type_name (A2 lesson: Go nil -> explicit NULL -> 23502).
- Backfills ARRAY[author]/ARRAY[publisher] — deliberately no
  comma-splitting ('Tolkien, J.R.R.' would corrupt; scanner kept only
  authors[0] historically).
- collection_items.position INTEGER + (collection_id, position) index
  for playlist ordering.

Go: sqlc regen; MediaMetadata/handler params gain Subtitle/Explicit/
Authors/Publishers (legacy singles still accepted, arrays win);
SeriesNumber int32 -> float64 end to end; metadata_overrides adapted
with legacy override keys ('author'/'publisher') preserved so existing
user overrides survive; bulk author edit writes the array.

Pins widened (docs/contract_pins.md): raw sqlc row key set + the six
new columns; changelog entry added.

Verified: unit suite green; integration sweep (bulk/smart collections/
scanner/stats/sync/queue/calibre/pins/audiobooks) green against the
shared dev DB after in-place migration. Pre-existing unrelated red:
TestOwnershipAndAdminTestSuite/TestAdminCreateUser timezone inheritance
fails identically at the pre-change commit (live shared state).
This commit is contained in:
John O'Keefe committed 2026-10-19 14:17:50 -04:00
1 parent f7dc8189a1
commit 646477afad
30 files changed
+790 -247

No files matched your search

+26 -19
View File
@@ -15,6 +15,7 @@ type CollectionItems struct {
AddedAt pgtype.Timestamptz `db:"added_at" json:"added_at"`
AddedByUserID pgtype.UUID `db:"added_by_user_id" json:"added_by_user_id"`
Excluded pgtype.Bool `db:"excluded" json:"excluded"`
Position pgtype.Int4 `db:"position" json:"position"`
}
type Collections struct {
@@ -232,25 +233,31 @@ type MediaItemFormats struct {
}
type MediaItems struct {
ID pgtype.UUID `db:"id" json:"id"`
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
Title string `db:"title" json:"title"`
Author pgtype.Text `db:"author" json:"author"`
Isbn pgtype.Text `db:"isbn" json:"isbn"`
Description pgtype.Text `db:"description" json:"description"`
FilePath string `db:"file_path" json:"file_path"`
FileSize pgtype.Int8 `db:"file_size" json:"file_size"`
MimeType pgtype.Text `db:"mime_type" json:"mime_type"`
CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"`
Series pgtype.Text `db:"series" json:"series"`
SeriesNumber pgtype.Int4 `db:"series_number" json:"series_number"`
Tags []string `db:"tags" json:"tags"`
Narrators []string `db:"narrators" json:"narrators"`
Abridged bool `db:"abridged" json:"abridged"`
Asin pgtype.Text `db:"asin" json:"asin"`
DatePublished pgtype.Date `db:"date_published" json:"date_published"`
Publisher pgtype.Text `db:"publisher" json:"publisher"`
Contributors []string `db:"contributors" json:"contributors"`
ID pgtype.UUID `db:"id" json:"id"`
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
Title string `db:"title" json:"title"`
Subtitle pgtype.Text `db:"subtitle" json:"subtitle"`
Author pgtype.Text `db:"author" json:"author"`
Isbn pgtype.Text `db:"isbn" json:"isbn"`
Description pgtype.Text `db:"description" json:"description"`
FilePath string `db:"file_path" json:"file_path"`
FileSize pgtype.Int8 `db:"file_size" json:"file_size"`
MimeType pgtype.Text `db:"mime_type" json:"mime_type"`
CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"`
Series pgtype.Text `db:"series" json:"series"`
SeriesNumber pgtype.Float8 `db:"series_number" json:"series_number"`
Tags []string `db:"tags" json:"tags"`
Narrators []string `db:"narrators" json:"narrators"`
Authors []string `db:"authors" json:"authors"`
AuthorsSearch []string `db:"authors_search" json:"authors_search"`
Publishers []string `db:"publishers" json:"publishers"`
PublishersSearch []string `db:"publishers_search" json:"publishers_search"`
Abridged bool `db:"abridged" json:"abridged"`
Explicit bool `db:"explicit" json:"explicit"`
Asin pgtype.Text `db:"asin" json:"asin"`
DatePublished pgtype.Date `db:"date_published" json:"date_published"`
Publisher pgtype.Text `db:"publisher" json:"publisher"`
Contributors []string `db:"contributors" json:"contributors"`
// ISO 639-1 language code (e.g., en, es, fr)
Language pgtype.Text `db:"language" json:"language"`
// Edition information (e.g., "First Edition", "Revised Edition")