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).
137 lines
6.6 KiB
Go
137 lines
6.6 KiB
Go
package services
|
|
|
|
import (
|
|
"bookhoard/internal/database"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestContinueSeriesRowToMediaItems_FieldsMappedCorrectly(t *testing.T) {
|
|
itemUUID := uuid.New()
|
|
libUUID := uuid.New()
|
|
adminUUID := uuid.New()
|
|
|
|
row := database.GetContinueSeriesItemsRow{
|
|
ID: pgtype.UUID{Bytes: itemUUID, Valid: true},
|
|
LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
|
|
Title: "Test Book",
|
|
Author: pgtype.Text{String: "Test Author", Valid: true},
|
|
Isbn: pgtype.Text{String: "978-1234567890", Valid: true},
|
|
Description: pgtype.Text{String: "A test book", Valid: true},
|
|
FilePath: "/books/test.epub",
|
|
FileSize: pgtype.Int8{Int64: 1024, Valid: true},
|
|
MimeType: pgtype.Text{String: "application/epub+zip", Valid: true},
|
|
CoverImagePath: pgtype.Text{String: "/covers/test.jpg", Valid: true},
|
|
Series: pgtype.Text{String: "Test Series", Valid: true},
|
|
SeriesNumber: pgtype.Float8{Float64: 3, Valid: true},
|
|
Tags: []string{"fantasy", "adventure"},
|
|
FormatGroup: "epub",
|
|
AddedByAdminID: pgtype.UUID{Bytes: adminUUID, Valid: true},
|
|
}
|
|
|
|
result := continueSeriesRowToMediaItems(row)
|
|
|
|
assert.Equal(t, pgtype.UUID{Bytes: itemUUID, Valid: true}, result.ID, "ID should match")
|
|
assert.Equal(t, pgtype.UUID{Bytes: libUUID, Valid: true}, result.LibraryID, "LibraryID should match")
|
|
assert.Equal(t, "Test Book", result.Title, "Title should match")
|
|
assert.Equal(t, pgtype.Text{String: "Test Author", Valid: true}, result.Author, "Author should match")
|
|
assert.Equal(t, pgtype.Text{String: "978-1234567890", Valid: true}, result.Isbn, "ISBN should match")
|
|
assert.Equal(t, "/books/test.epub", result.FilePath, "FilePath should match")
|
|
assert.Equal(t, pgtype.Text{String: "application/epub+zip", Valid: true}, result.MimeType, "MimeType should match")
|
|
assert.Equal(t, pgtype.Text{String: "/covers/test.jpg", Valid: true}, result.CoverImagePath, "CoverImagePath should match")
|
|
assert.Equal(t, pgtype.Text{String: "Test Series", Valid: true}, result.Series, "Series should match")
|
|
assert.Equal(t, pgtype.Float8{Float64: 3, Valid: true}, result.SeriesNumber, "SeriesNumber should match")
|
|
assert.Equal(t, []string{"fantasy", "adventure"}, result.Tags, "Tags should match")
|
|
assert.Equal(t, "epub", result.FormatGroup, "FormatGroup should match")
|
|
}
|
|
|
|
func TestContinueSeriesRowToMediaItems_NullFieldsHandled(t *testing.T) {
|
|
itemUUID := uuid.New()
|
|
libUUID := uuid.New()
|
|
|
|
row := database.GetContinueSeriesItemsRow{
|
|
ID: pgtype.UUID{Bytes: itemUUID, Valid: true},
|
|
LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
|
|
Title: "No Metadata Book",
|
|
Author: pgtype.Text{Valid: false},
|
|
Isbn: pgtype.Text{Valid: false},
|
|
Description: pgtype.Text{Valid: false},
|
|
FilePath: "/books/nometa.epub",
|
|
FileSize: pgtype.Int8{Valid: false},
|
|
MimeType: pgtype.Text{Valid: false},
|
|
CoverImagePath: pgtype.Text{Valid: false},
|
|
Series: pgtype.Text{Valid: false},
|
|
SeriesNumber: pgtype.Float8{Valid: false},
|
|
Tags: nil,
|
|
FormatGroup: "epub",
|
|
}
|
|
|
|
result := continueSeriesRowToMediaItems(row)
|
|
|
|
assert.Equal(t, "No Metadata Book", result.Title)
|
|
assert.False(t, result.Author.Valid, "Author should be invalid/null")
|
|
assert.False(t, result.Series.Valid, "Series should be invalid/null")
|
|
assert.False(t, result.SeriesNumber.Valid, "SeriesNumber should be invalid/null")
|
|
assert.Nil(t, result.Tags, "Tags should be nil")
|
|
}
|
|
|
|
func TestContinueSeriesRowToMediaItems_AllFieldsMapped(t *testing.T) {
|
|
itemUUID := uuid.New()
|
|
libUUID := uuid.New()
|
|
|
|
row := database.GetContinueSeriesItemsRow{
|
|
ID: pgtype.UUID{Bytes: itemUUID, Valid: true},
|
|
LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
|
|
Title: "Full Book",
|
|
Author: pgtype.Text{String: "Author", Valid: true},
|
|
Isbn: pgtype.Text{String: "ISBN", Valid: true},
|
|
Description: pgtype.Text{String: "Desc", Valid: true},
|
|
FilePath: "/book.epub",
|
|
FileSize: pgtype.Int8{Int64: 2048, Valid: true},
|
|
MimeType: pgtype.Text{String: "epub", Valid: true},
|
|
CoverImagePath: pgtype.Text{String: "/cover.jpg", Valid: true},
|
|
Series: pgtype.Text{String: "Series", Valid: true},
|
|
SeriesNumber: pgtype.Float8{Float64: 1, Valid: true},
|
|
Tags: []string{"tag1"},
|
|
Asin: pgtype.Text{String: "ASIN", Valid: true},
|
|
Publisher: pgtype.Text{String: "Pub", Valid: true},
|
|
Language: pgtype.Text{String: "en", Valid: true},
|
|
Edition: pgtype.Text{String: "1st", Valid: true},
|
|
Genre: pgtype.Text{String: "Fiction", Valid: true},
|
|
FormatGroup: "epub",
|
|
FormatMimetype: pgtype.Text{String: "application/epub+zip", Valid: true},
|
|
IsReflowable: pgtype.Bool{Bool: true, Valid: true},
|
|
HasFixedLayout: pgtype.Bool{Bool: false, Valid: true},
|
|
TotalCharacters: pgtype.Int8{Int64: 500000, Valid: true},
|
|
ChapterCount: pgtype.Int4{Int32: 20, Valid: true},
|
|
MangaType: pgtype.Text{String: "manga", Valid: true},
|
|
ReadingDirection: pgtype.Text{String: "rtl", Valid: true},
|
|
SeriesCount: pgtype.Int4{Int32: 10, Valid: true},
|
|
Volume: pgtype.Int4{Int32: 1, Valid: true},
|
|
LibraryTypeName: pgtype.Text{String: "ebooks", Valid: true},
|
|
FileSha256: pgtype.Text{String: "abc123", Valid: true},
|
|
}
|
|
|
|
result := continueSeriesRowToMediaItems(row)
|
|
|
|
assert.Equal(t, pgtype.Text{String: "ASIN", Valid: true}, result.Asin)
|
|
assert.Equal(t, pgtype.Text{String: "Pub", Valid: true}, result.Publisher)
|
|
assert.Equal(t, pgtype.Text{String: "en", Valid: true}, result.Language)
|
|
assert.Equal(t, pgtype.Text{String: "1st", Valid: true}, result.Edition)
|
|
assert.Equal(t, pgtype.Text{String: "Fiction", Valid: true}, result.Genre)
|
|
assert.Equal(t, pgtype.Text{String: "application/epub+zip", Valid: true}, result.FormatMimetype)
|
|
assert.Equal(t, pgtype.Bool{Bool: true, Valid: true}, result.IsReflowable)
|
|
assert.Equal(t, pgtype.Bool{Bool: false, Valid: true}, result.HasFixedLayout)
|
|
assert.Equal(t, pgtype.Int8{Int64: 500000, Valid: true}, result.TotalCharacters)
|
|
assert.Equal(t, pgtype.Int4{Int32: 20, Valid: true}, result.ChapterCount)
|
|
assert.Equal(t, pgtype.Text{String: "manga", Valid: true}, result.MangaType)
|
|
assert.Equal(t, pgtype.Text{String: "rtl", Valid: true}, result.ReadingDirection)
|
|
assert.Equal(t, pgtype.Int4{Int32: 10, Valid: true}, result.SeriesCount)
|
|
assert.Equal(t, pgtype.Int4{Int32: 1, Valid: true}, result.Volume)
|
|
assert.Equal(t, pgtype.Text{String: "ebooks", Valid: true}, result.LibraryTypeName)
|
|
assert.Equal(t, pgtype.Text{String: "abc123", Valid: true}, result.FileSha256)
|
|
}
|