From 646477afad64ac2b77a8e18b8c48ab73f0240453 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Mon, 19 Oct 2026 14:17:50 -0400 Subject: [PATCH] feat(schema): subtitle/explicit columns, authors+publishers arrays, float series numbers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- cmd/server/tests/calibre_integration_test.go | 2 +- cmd/server/tests/collections_preview_test.go | 2 +- cmd/server/tests/collections_smart_test.go | 2 +- cmd/server/tests/contract_pins_test.go | 8 +- cmd/server/tests/ownership_and_admin_test.go | 2 +- cmd/server/tests/queue_test.go | 2 +- cmd/server/tests/statistics_test.go | 2 +- cmd/server/tests/sync_integration_test.go | 2 +- database/schema/schema.sql | 86 ++- docs/contract_pins.md | 17 + internal/database/models.go | 45 +- internal/database/querier.go | 2 +- internal/database/queries.sql.go | 578 ++++++++++++++---- internal/database/queries/queries.sql | 94 +-- internal/handlers/media.go | 43 +- internal/services/media_scanner.go | 48 +- internal/services/media_scanner_audio.go | 4 +- .../media_scanner_audio_metadata_test.go | 2 +- .../services/media_scanner_audio_multifile.go | 10 +- .../services/media_scanner_metadata_test.go | 8 +- internal/services/media_scanner_opf_test.go | 4 +- internal/services/series_service_test.go | 8 +- internal/utils/metadata_overrides.go | 23 +- internal/utils/metadata_overrides_test.go | 4 +- internal/utils/tags.go | 15 + templates/book_detail.templ | 4 +- templates/book_detail_modals.templ | 2 +- templates/book_detail_modals_templ.go | 4 +- templates/book_detail_templ.go | 6 +- templates/utils.go | 8 + 30 files changed, 790 insertions(+), 247 deletions(-) diff --git a/cmd/server/tests/calibre_integration_test.go b/cmd/server/tests/calibre_integration_test.go index 71079487..c2fb6ff9 100644 --- a/cmd/server/tests/calibre_integration_test.go +++ b/cmd/server/tests/calibre_integration_test.go @@ -108,7 +108,7 @@ func TestCalibreLibraryScan(t *testing.T) { require.Equal(t, "Test Book", book.Title, "Title should match sidecar") require.Equal(t, "Test Author", book.Author.String, "Author should match sidecar") require.Equal(t, "Test Series", book.Series.String, "Series should match sidecar") - require.Equal(t, int32(1), book.SeriesNumber.Int32, "Series number should match sidecar") + require.Equal(t, float64(1), book.SeriesNumber.Float64, "Series number should match sidecar") require.Equal(t, "Test Publisher", book.Publisher.String, "Publisher should match sidecar") require.Len(t, book.Tags, 2, "Should have 2 tags from sidecar") require.Contains(t, book.Tags, "Fantasy", "Should have Fantasy tag") diff --git a/cmd/server/tests/collections_preview_test.go b/cmd/server/tests/collections_preview_test.go index 86db2400..97e7c941 100644 --- a/cmd/server/tests/collections_preview_test.go +++ b/cmd/server/tests/collections_preview_test.go @@ -382,7 +382,7 @@ func createPreviewTestMediaItems(t *testing.T, db *database.Queries, libraryID s media, err := db.CreateMediaItem(ctx, database.CreateMediaItemParams{ LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true}, Title: "Preview Test Book " + string(rune('A'+i)), - Author: pgtype.Text{String: "Test Author " + string(rune('A'+i)), Valid: true}, + Authors: []string{"Test Author " + string(rune('A'+i))}, Genre: pgtype.Text{String: genre, Valid: true}, FilePath: "/tmp/test" + string(rune('A'+i)) + ".epub", FileSize: pgtype.Int8{Int64: 1024, Valid: true}, diff --git a/cmd/server/tests/collections_smart_test.go b/cmd/server/tests/collections_smart_test.go index e944ab2b..d62cdd7b 100644 --- a/cmd/server/tests/collections_smart_test.go +++ b/cmd/server/tests/collections_smart_test.go @@ -111,7 +111,7 @@ func smartCreateLibraryAndBooks(t *testing.T, setup *TestServerSetup, userID uui media, err := setup.DB.CreateMediaItem(ctx, database.CreateMediaItemParams{ LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true}, Title: title, - Author: pgtype.Text{String: "Smart Rule Author", Valid: true}, + Authors: []string{"Smart Rule Author"}, Genre: pgtype.Text{String: genre, Valid: true}, FilePath: fmt.Sprintf("/tmp/%s-%d.epub", title, time.Now().UnixNano()), FileSize: pgtype.Int8{Int64: 1024, Valid: true}, diff --git a/cmd/server/tests/contract_pins_test.go b/cmd/server/tests/contract_pins_test.go index be95e42d..b9fc536b 100644 --- a/cmd/server/tests/contract_pins_test.go +++ b/cmd/server/tests/contract_pins_test.go @@ -123,7 +123,7 @@ func createContractFixture(t *testing.T, setup *TestServerSetup) contractFixture media, err := setup.DB.CreateMediaItem(ctx, database.CreateMediaItemParams{ LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true}, Title: itemTitle, - Author: pgtype.Text{String: "Test Contract Author", Valid: true}, + Authors: []string{"Test Contract Author"}, Series: pgtype.Text{String: seriesName, Valid: true}, Genre: pgtype.Text{String: "Science Fiction", Valid: true}, FilePath: "/tmp/contract-test-item.epub", @@ -711,10 +711,14 @@ func TestContractPin_MediaListBothBranches(t *testing.T) { fx := createContractFixture(t, setup) // The sqlc row key sets behind BOTH branches. + // A2b (2026-10-19): + subtitle, explicit, authors, authors_search, + // publishers, publishers_search (docs/contract_pins.md changelog). rawRowKeys := []string{ - "id", "library_id", "title", "author", "isbn", "description", + "id", "library_id", "title", "subtitle", "author", "isbn", "description", "file_path", "file_size", "mime_type", "cover_image_path", "series", "series_number", "tags", "narrators", "abridged", + "explicit", "authors", "authors_search", "publishers", + "publishers_search", "asin", "date_published", "publisher", "contributors", "language", "edition", "page_count", "genre", "copyright_year", "goodreads_id", "openlibrary_id", "google_books_id", "added_by_admin_id", "created_at", diff --git a/cmd/server/tests/ownership_and_admin_test.go b/cmd/server/tests/ownership_and_admin_test.go index ff64dc4a..accdda06 100644 --- a/cmd/server/tests/ownership_and_admin_test.go +++ b/cmd/server/tests/ownership_and_admin_test.go @@ -88,7 +88,7 @@ func (s *OwnershipAndAdminTestSuite) createItemViaDB(withFolder bool) (itemID, l item, err := db.CreateMediaItem(ctx, database.CreateMediaItemParams{ LibraryID: library.ID, Title: "Ownership Suite Item", - Author: pgtype.Text{String: "Suite Author", Valid: true}, + Authors: []string{"Suite Author"}, FilePath: relPath, MimeType: pgtype.Text{String: "application/epub+zip", Valid: true}, }) diff --git a/cmd/server/tests/queue_test.go b/cmd/server/tests/queue_test.go index 69ef921b..82621ae6 100644 --- a/cmd/server/tests/queue_test.go +++ b/cmd/server/tests/queue_test.go @@ -51,7 +51,7 @@ func createTestQueueItem(t *testing.T, db *database.Queries, userID uuid.UUID) ( mediaItem, err := db.CreateMediaItem(ctx, database.CreateMediaItemParams{ LibraryID: library.ID, Title: "Test Book for Queue", - Author: pgtype.Text{String: "Test Author", Valid: true}, + Authors: []string{"Test Author"}, FilePath: "/tmp/test.epub", FileSize: pgtype.Int8{Int64: 1024, Valid: true}, MimeType: pgtype.Text{String: "application/epub+zip", Valid: true}, diff --git a/cmd/server/tests/statistics_test.go b/cmd/server/tests/statistics_test.go index 491d1b2d..705c6177 100644 --- a/cmd/server/tests/statistics_test.go +++ b/cmd/server/tests/statistics_test.go @@ -544,7 +544,7 @@ func TestLibraryUsageStats(t *testing.T) { media, err := setup.DB.CreateMediaItem(ctx, database.CreateMediaItemParams{ LibraryID: pgtype.UUID{Bytes: [16]byte(lib), Valid: true}, Title: title, - Author: pgtype.Text{String: author, Valid: true}, + Authors: []string{author}, Genre: pgtype.Text{String: genre, Valid: true}, FilePath: fmt.Sprintf("/tmp/usage-%s-%d.bin", title, time.Now().UnixNano()), FileSize: pgtype.Int8{Int64: size, Valid: true}, diff --git a/cmd/server/tests/sync_integration_test.go b/cmd/server/tests/sync_integration_test.go index 652e4513..6a5bc8a0 100644 --- a/cmd/server/tests/sync_integration_test.go +++ b/cmd/server/tests/sync_integration_test.go @@ -95,7 +95,7 @@ func createSyncTestMedia(t *testing.T, db *database.Queries, libraryID pgtype.UU media, err := db.CreateMediaItem(ctx, database.CreateMediaItemParams{ LibraryID: libraryID, Title: "Sync Test Book", - Author: pgtype.Text{String: "Test Author", Valid: true}, + Authors: []string{"Test Author"}, FilePath: "/tmp/test.epub", FileSize: pgtype.Int8{Int64: 1024, Valid: true}, MimeType: pgtype.Text{String: "application/epub+zip", Valid: true}, diff --git a/database/schema/schema.sql b/database/schema/schema.sql index 423afd90..18f685d1 100644 --- a/database/schema/schema.sql +++ b/database/schema/schema.sql @@ -136,7 +136,8 @@ CREATE TABLE IF NOT EXISTS media_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), library_id UUID NOT NULL REFERENCES libraries(id) ON DELETE CASCADE, title VARCHAR(255) NOT NULL, - author VARCHAR(255), + subtitle TEXT, -- Series-y/edition subtitle from sidecars; display field, never part of `title` + author VARCHAR(255), -- DEPRECATED for writes: display string regenerated from `authors` by trigger isbn VARCHAR(13), -- Still relevant for ebooks description TEXT, file_path VARCHAR(500) NOT NULL, @@ -144,10 +145,15 @@ CREATE TABLE IF NOT EXISTS media_items ( mime_type VARCHAR(100), cover_image_path VARCHAR(500), series VARCHAR(255), - series_number INTEGER, + series_number DOUBLE PRECISION, -- Real ordinals are decimals (0.5, 1.5 novellas); was INTEGER tags TEXT[], narrators TEXT[], -- Audiobook narrators (sidecar first; user-editable) + authors TEXT[] NOT NULL DEFAULT '{}', -- Write truth for authorship; `author` above is trigger-regenerated display + authors_search TEXT[] NOT NULL DEFAULT '{}', + publishers TEXT[] NOT NULL DEFAULT '{}', -- Write truth; `publisher` below is trigger-regenerated display + publishers_search TEXT[] NOT NULL DEFAULT '{}', abridged BOOLEAN NOT NULL DEFAULT FALSE, -- Libation/Audible abridged flag + explicit BOOLEAN NOT NULL DEFAULT FALSE, -- Adult flag from sidecars (ABS explicit / Audible isAdult) asin VARCHAR(20), -- Still relevant for ebooks date_published DATE, publisher VARCHAR(255), @@ -267,6 +273,30 @@ ALTER TABLE media_items ADD COLUMN IF NOT EXISTS contributors_search TEXT[]; ALTER TABLE media_items ADD COLUMN IF NOT EXISTS narrators TEXT[] NOT NULL DEFAULT '{}'; ALTER TABLE media_items ADD COLUMN IF NOT EXISTS narrators_search TEXT[] NOT NULL DEFAULT '{}'; ALTER TABLE media_items ADD COLUMN IF NOT EXISTS abridged BOOLEAN NOT NULL DEFAULT FALSE; + +-- A2b: subtitle + explicit flags, authors/publishers arrays (narrators +-- precedent — arrays are write truth, varchar display columns regenerate). +ALTER TABLE media_items ADD COLUMN IF NOT EXISTS subtitle TEXT; +ALTER TABLE media_items ADD COLUMN IF NOT EXISTS explicit BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE media_items ADD COLUMN IF NOT EXISTS authors TEXT[] NOT NULL DEFAULT '{}'; +ALTER TABLE media_items ADD COLUMN IF NOT EXISTS authors_search TEXT[] NOT NULL DEFAULT '{}'; +ALTER TABLE media_items ADD COLUMN IF NOT EXISTS publishers TEXT[] NOT NULL DEFAULT '{}'; +ALTER TABLE media_items ADD COLUMN IF NOT EXISTS publishers_search TEXT[] NOT NULL DEFAULT '{}'; + +-- Series ordinals are real-world decimals (0.5, 1.5 novellas); INTEGER +-- truncated them to the wrong shelf order. In-place widening; no-op on a +-- fresh install (column is already double precision from CREATE TABLE). +ALTER TABLE media_items ALTER COLUMN series_number TYPE double precision + USING series_number::double precision; + +-- Backfill the arrays from the legacy single-value columns. Deliberately NO +-- comma-splitting ("Tolkien, J.R.R." would corrupt); every existing row is +-- effectively single-valued because the scanner historically kept only +-- authors[0]. Guarded so the pass is free after the first startup. +UPDATE media_items SET authors = ARRAY[author] + WHERE author IS NOT NULL AND author <> '' AND authors = '{}'; +UPDATE media_items SET publishers = ARRAY[publisher] + WHERE publisher IS NOT NULL AND publisher <> '' AND publishers = '{}'; -- audio_metadata is declared in the CREATE TABLE above, which only fresh -- installs ever see — this ALTER is the upgrade path for pre-A2 installs -- (its absence 42703'd every scan and archive pass on upgraded deployments). @@ -316,6 +346,20 @@ BEGIN IF NEW.narrators_search IS NULL THEN NEW.narrators_search := '{}'; END IF; + -- A2b: authors/publishers arrays are NOT NULL — Go nil slices become + -- explicit NULLs in params and would 23502 (the narrators lesson). + IF NEW.authors IS NULL THEN + NEW.authors := '{}'; + END IF; + IF NEW.authors_search IS NULL THEN + NEW.authors_search := '{}'; + END IF; + IF NEW.publishers IS NULL THEN + NEW.publishers := '{}'; + END IF; + IF NEW.publishers_search IS NULL THEN + NEW.publishers_search := '{}'; + END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; @@ -333,6 +377,35 @@ CREATE TRIGGER set_library_type_name_on_library_change FOR EACH ROW EXECUTE FUNCTION set_library_type_name(); +-- Display sync: `author`/`publisher` are denormalized display strings +-- regenerated from the authors/publishers arrays on every write. The varchar +-- columns face search (trgm), facets and API responses; the arrays are the +-- write truth. array_to_string is STABLE on PG15 (not IMMUTABLE), so this +-- must be a trigger — a generated column would reject it. Absent arrays +-- yield NULL display (matches the legacy no-author row shape). +DROP TRIGGER IF EXISTS sync_author_publisher_display_trg ON media_items; +DROP FUNCTION IF EXISTS sync_author_publisher_display(); +CREATE OR REPLACE FUNCTION sync_author_publisher_display() RETURNS TRIGGER AS $$ +BEGIN + -- Defense in depth: normalize NULL arrays first (this trigger may fire + -- before set_library_type_name depending on name-ordered execution). + IF NEW.authors IS NULL THEN + NEW.authors := '{}'; + END IF; + IF NEW.publishers IS NULL THEN + NEW.publishers := '{}'; + END IF; + NEW.author := CASE WHEN NEW.authors = '{}' THEN NULL + ELSE array_to_string(NEW.authors, ', ') END; + NEW.publisher := CASE WHEN NEW.publishers = '{}' THEN NULL + ELSE array_to_string(NEW.publishers, ', ') END; + RETURN NEW; +END $$ LANGUAGE plpgsql; +CREATE TRIGGER sync_author_publisher_display_trg + BEFORE INSERT OR UPDATE ON media_items + FOR EACH ROW + EXECUTE FUNCTION sync_author_publisher_display(); + -- Add GIN indexes for pg_trgm fuzzy search performance CREATE INDEX IF NOT EXISTS idx_media_items_author_trgm ON media_items USING GIN (author gin_trgm_ops); @@ -1256,12 +1329,21 @@ CREATE TABLE IF NOT EXISTS collection_items ( added_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), added_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL, excluded BOOLEAN DEFAULT false, + -- Manual playlist ordering (reading-order collections). NULL = un-ordered + -- legacy item; ordering falls back to added_at ASC. + position INTEGER, UNIQUE(collection_id, media_item_id) ); CREATE INDEX IF NOT EXISTS idx_collection_items_collection ON collection_items(collection_id); CREATE INDEX IF NOT EXISTS idx_collection_items_media ON collection_items(media_item_id); +-- Playlist ordering (A2c): position-first ordering for collection listings; +-- NULLs fall back to added_at ASC. +ALTER TABLE collection_items ADD COLUMN IF NOT EXISTS position INTEGER; +CREATE INDEX IF NOT EXISTS idx_collection_items_order + ON collection_items(collection_id, position ASC NULLS LAST); + -- Index for excluding auto-assigned items CREATE INDEX IF NOT EXISTS idx_collection_items_excluded ON collection_items(collection_id, excluded) WHERE excluded = true; diff --git a/docs/contract_pins.md b/docs/contract_pins.md index 421e857a..9f011b96 100644 --- a/docs/contract_pins.md +++ b/docs/contract_pins.md @@ -119,3 +119,20 @@ cover_image_path, has_conflict, format_group, badge`. - `media_items.audio_metadata` payload gained additive JSON keys `layout` and `tracks` (see schema.sql comment). Consumers parse additively; stored single-file payloads are unchanged. + +## 2026-10-19 — A2b: metadata array/model overhaul (phase 1) + +- `media_items.series_number` widened INTEGER → DOUBLE PRECISION across all + surfaces. JSON serializers emit the same key; values are JSON numbers, so + integer-valued ordinals are byte-identical ("3") and decimal ordinals + (0.5/1.5) now serialize as decimals instead of truncating. Clients reading + JSON numbers are unaffected; the Android client parses either form. +- `GET /api/media-items*` / detail responses gain ADDITIVE keys: + `subtitle` (string), `explicit` (bool), `authors` ([]string, write truth — + legacy `author` display string still emitted, now trigger-regenerated), + `publishers` ([]string; legacy `publisher` likewise). +- `POST/PUT /api/media-items*` accept ADDITIVE request keys `subtitle`, + `explicit`, `authors`, `publishers`; legacy `author`/`publisher` singles + still accepted (arrays win when both present). +- Override keys unchanged: `author`/`publisher` override names are preserved + for existing rows and now protect the arrays. diff --git a/internal/database/models.go b/internal/database/models.go index 9b6ac156..e1d77ff2 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -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") diff --git a/internal/database/querier.go b/internal/database/querier.go index 7271861e..bf4d9104 100644 --- a/internal/database/querier.go +++ b/internal/database/querier.go @@ -583,7 +583,7 @@ type Querier interface { // books): rescans recompute the track manifest and the aggregate facts // that follow from it. Editorial fields stay untouched — admins may have // edited title/narrators after ingest. - UpdateMediaItemAudioBookPayload(ctx context.Context, arg UpdateMediaItemAudioBookPayloadParams) (MediaItems, error) + UpdateMediaItemAudioBookPayload(ctx context.Context, arg UpdateMediaItemAudioBookPayloadParams) (UpdateMediaItemAudioBookPayloadRow, error) UpdateMediaItemChapterMetadata(ctx context.Context, arg UpdateMediaItemChapterMetadataParams) (MediaItems, error) // Update media item format UpdateMediaItemFormat(ctx context.Context, arg UpdateMediaItemFormatParams) (MediaItemFormats, error) diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index 222e9822..32fa577e 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -16,7 +16,7 @@ const AddBookToCollection = `-- name: AddBookToCollection :one INSERT INTO collection_items (collection_id, media_item_id, added_by_user_id) VALUES ($1, $2, $3) ON CONFLICT (collection_id, media_item_id) DO NOTHING -RETURNING id, collection_id, media_item_id, added_at, added_by_user_id, excluded +RETURNING id, collection_id, media_item_id, added_at, added_by_user_id, excluded, position ` type AddBookToCollectionParams struct { @@ -37,6 +37,7 @@ func (q *Queries) AddBookToCollection(ctx context.Context, arg AddBookToCollecti &i.AddedAt, &i.AddedByUserID, &i.Excluded, + &i.Position, ) return i, err } @@ -880,16 +881,18 @@ func (q *Queries) CreateMediaHighlightFull(ctx context.Context, arg CreateMediaH } const CreateMediaItem = `-- name: CreateMediaItem :one -INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publisher, contributors, contributors_search, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, library_type_name, audio_metadata, narrators, narrators_search, abridged) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48) +INSERT INTO media_items (library_id, title, subtitle, authors, authors_search, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publishers, publishers_search, contributors, contributors_search, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, library_type_name, audio_metadata, narrators, narrators_search, abridged, explicit) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51, $52) ON CONFLICT (library_id, file_path) DO UPDATE SET updated_at = NOW() -RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, abridged, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence +RETURNING id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence ` type CreateMediaItemParams struct { LibraryID pgtype.UUID `db:"library_id" json:"library_id"` Title string `db:"title" json:"title"` - Author pgtype.Text `db:"author" json:"author"` + Subtitle pgtype.Text `db:"subtitle" json:"subtitle"` + Authors []string `db:"authors" json:"authors"` + AuthorsSearch []string `db:"authors_search" json:"authors_search"` Isbn pgtype.Text `db:"isbn" json:"isbn"` Description pgtype.Text `db:"description" json:"description"` FilePath string `db:"file_path" json:"file_path"` @@ -897,12 +900,13 @@ type CreateMediaItemParams struct { 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"` + SeriesNumber pgtype.Float8 `db:"series_number" json:"series_number"` Tags []string `db:"tags" json:"tags"` TagsSearch []string `db:"tags_search" json:"tags_search"` Asin pgtype.Text `db:"asin" json:"asin"` DatePublished pgtype.Date `db:"date_published" json:"date_published"` - Publisher pgtype.Text `db:"publisher" json:"publisher"` + Publishers []string `db:"publishers" json:"publishers"` + PublishersSearch []string `db:"publishers_search" json:"publishers_search"` Contributors []string `db:"contributors" json:"contributors"` ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` Language pgtype.Text `db:"language" json:"language"` @@ -935,6 +939,7 @@ type CreateMediaItemParams struct { Narrators []string `db:"narrators" json:"narrators"` NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` Abridged bool `db:"abridged" json:"abridged"` + Explicit bool `db:"explicit" json:"explicit"` } // Media Items queries @@ -945,7 +950,9 @@ func (q *Queries) CreateMediaItem(ctx context.Context, arg CreateMediaItemParams row := q.db.QueryRow(ctx, CreateMediaItem, arg.LibraryID, arg.Title, - arg.Author, + arg.Subtitle, + arg.Authors, + arg.AuthorsSearch, arg.Isbn, arg.Description, arg.FilePath, @@ -958,7 +965,8 @@ func (q *Queries) CreateMediaItem(ctx context.Context, arg CreateMediaItemParams arg.TagsSearch, arg.Asin, arg.DatePublished, - arg.Publisher, + arg.Publishers, + arg.PublishersSearch, arg.Contributors, arg.ContributorsSearch, arg.Language, @@ -991,12 +999,14 @@ func (q *Queries) CreateMediaItem(ctx context.Context, arg CreateMediaItemParams arg.Narrators, arg.NarratorsSearch, arg.Abridged, + arg.Explicit, ) var i MediaItems err := row.Scan( &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -1008,7 +1018,12 @@ func (q *Queries) CreateMediaItem(ctx context.Context, arg CreateMediaItemParams &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -1116,16 +1131,18 @@ func (q *Queries) CreateMediaItemFormat(ctx context.Context, arg CreateMediaItem } const CreateMediaItemIfNew = `-- name: CreateMediaItemIfNew :one -INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publisher, contributors, contributors_search, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, library_type_name, audio_metadata, narrators, narrators_search, abridged) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48) +INSERT INTO media_items (library_id, title, subtitle, authors, authors_search, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publishers, publishers_search, contributors, contributors_search, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, library_type_name, audio_metadata, narrators, narrators_search, abridged, explicit) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51, $52) ON CONFLICT (library_id, file_path) DO NOTHING -RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, abridged, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence +RETURNING id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence ` type CreateMediaItemIfNewParams struct { LibraryID pgtype.UUID `db:"library_id" json:"library_id"` Title string `db:"title" json:"title"` - Author pgtype.Text `db:"author" json:"author"` + Subtitle pgtype.Text `db:"subtitle" json:"subtitle"` + Authors []string `db:"authors" json:"authors"` + AuthorsSearch []string `db:"authors_search" json:"authors_search"` Isbn pgtype.Text `db:"isbn" json:"isbn"` Description pgtype.Text `db:"description" json:"description"` FilePath string `db:"file_path" json:"file_path"` @@ -1133,12 +1150,13 @@ type CreateMediaItemIfNewParams struct { 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"` + SeriesNumber pgtype.Float8 `db:"series_number" json:"series_number"` Tags []string `db:"tags" json:"tags"` TagsSearch []string `db:"tags_search" json:"tags_search"` Asin pgtype.Text `db:"asin" json:"asin"` DatePublished pgtype.Date `db:"date_published" json:"date_published"` - Publisher pgtype.Text `db:"publisher" json:"publisher"` + Publishers []string `db:"publishers" json:"publishers"` + PublishersSearch []string `db:"publishers_search" json:"publishers_search"` Contributors []string `db:"contributors" json:"contributors"` ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` Language pgtype.Text `db:"language" json:"language"` @@ -1171,6 +1189,7 @@ type CreateMediaItemIfNewParams struct { Narrators []string `db:"narrators" json:"narrators"` NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` Abridged bool `db:"abridged" json:"abridged"` + Explicit bool `db:"explicit" json:"explicit"` } // API create path: race-safe create-or-detect. On a duplicate @@ -1183,7 +1202,9 @@ func (q *Queries) CreateMediaItemIfNew(ctx context.Context, arg CreateMediaItemI row := q.db.QueryRow(ctx, CreateMediaItemIfNew, arg.LibraryID, arg.Title, - arg.Author, + arg.Subtitle, + arg.Authors, + arg.AuthorsSearch, arg.Isbn, arg.Description, arg.FilePath, @@ -1196,7 +1217,8 @@ func (q *Queries) CreateMediaItemIfNew(ctx context.Context, arg CreateMediaItemI arg.TagsSearch, arg.Asin, arg.DatePublished, - arg.Publisher, + arg.Publishers, + arg.PublishersSearch, arg.Contributors, arg.ContributorsSearch, arg.Language, @@ -1229,12 +1251,14 @@ func (q *Queries) CreateMediaItemIfNew(ctx context.Context, arg CreateMediaItemI arg.Narrators, arg.NarratorsSearch, arg.Abridged, + arg.Explicit, ) var i MediaItems err := row.Scan( &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -1246,7 +1270,12 @@ func (q *Queries) CreateMediaItemIfNew(ctx context.Context, arg CreateMediaItemI &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -2663,7 +2692,7 @@ func (q *Queries) GetBookReadingSessions(ctx context.Context, arg GetBookReading } const GetBooksByTag = `-- name: GetBooksByTag :many -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, abridged, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE library_id = $1 AND tags @> ARRAY[$2::text] ORDER BY title ASC ` @@ -2686,6 +2715,7 @@ func (q *Queries) GetBooksByTag(ctx context.Context, arg GetBooksByTagParams) ([ &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -2697,7 +2727,12 @@ func (q *Queries) GetBooksByTag(ctx context.Context, arg GetBooksByTagParams) ([ &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -2790,7 +2825,7 @@ func (q *Queries) GetCollection(ctx context.Context, id pgtype.UUID) (Collection } const GetCollectionItems = `-- name: GetCollectionItems :many -SELECT ci.id, ci.collection_id, ci.media_item_id, ci.added_at, ci.added_by_user_id, ci.excluded, mi.title, mi.author, mi.cover_image_path, mi.library_id, mi.format_group +SELECT ci.id, ci.collection_id, ci.media_item_id, ci.added_at, ci.added_by_user_id, ci.excluded, ci.position, mi.title, mi.author, mi.cover_image_path, mi.library_id, mi.format_group FROM collection_items ci JOIN media_items mi ON ci.media_item_id = mi.id WHERE ci.collection_id = $1 @@ -2806,6 +2841,7 @@ type GetCollectionItemsRow 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"` Title string `db:"title" json:"title"` Author pgtype.Text `db:"author" json:"author"` CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` @@ -2830,6 +2866,7 @@ func (q *Queries) GetCollectionItems(ctx context.Context, collectionID pgtype.UU &i.AddedAt, &i.AddedByUserID, &i.Excluded, + &i.Position, &i.Title, &i.Author, &i.CoverImagePath, @@ -2847,7 +2884,7 @@ func (q *Queries) GetCollectionItems(ctx context.Context, collectionID pgtype.UU } const GetCollectionItemsForDashboard = `-- name: GetCollectionItemsForDashboard :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.abridged, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi INNER JOIN collection_items ci ON ci.media_item_id = mi.id WHERE ci.collection_id = $1 AND ($2::uuid IS NULL OR mi.library_id = $2::uuid) @@ -2884,6 +2921,7 @@ func (q *Queries) GetCollectionItemsForDashboard(ctx context.Context, arg GetCol &i.MediaItems.ID, &i.MediaItems.LibraryID, &i.MediaItems.Title, + &i.MediaItems.Subtitle, &i.MediaItems.Author, &i.MediaItems.Isbn, &i.MediaItems.Description, @@ -2895,7 +2933,12 @@ func (q *Queries) GetCollectionItemsForDashboard(ctx context.Context, arg GetCol &i.MediaItems.SeriesNumber, &i.MediaItems.Tags, &i.MediaItems.Narrators, + &i.MediaItems.Authors, + &i.MediaItems.AuthorsSearch, + &i.MediaItems.Publishers, + &i.MediaItems.PublishersSearch, &i.MediaItems.Abridged, + &i.MediaItems.Explicit, &i.MediaItems.Asin, &i.MediaItems.DatePublished, &i.MediaItems.Publisher, @@ -3097,7 +3140,7 @@ func (q *Queries) GetCollectionsForBook(ctx context.Context, mediaItemID pgtype. } const GetContinueReadingItems = `-- name: GetContinueReadingItems :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.abridged, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi INNER JOIN ( SELECT DISTINCT ON (media_item_id) media_item_id, last_read_at FROM reading_progress @@ -3133,6 +3176,7 @@ func (q *Queries) GetContinueReadingItems(ctx context.Context, arg GetContinueRe &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -3144,7 +3188,12 @@ func (q *Queries) GetContinueReadingItems(ctx context.Context, arg GetContinueRe &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -3223,7 +3272,7 @@ WITH user_series_progress AS ( GROUP BY mi.series ), next_books AS ( - SELECT DISTINCT ON (mi.series) mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.abridged, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, + SELECT DISTINCT ON (mi.series) mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, usp.last_read_at FROM media_items mi JOIN user_series_progress usp ON mi.series = usp.series @@ -3233,7 +3282,7 @@ next_books AS ( AND (mi.series_number > usp.max_read_number OR usp.max_read_number IS NULL) ORDER BY mi.series, mi.series_number ASC NULLS LAST ) -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, abridged, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, last_read_at FROM next_books +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, last_read_at FROM next_books ORDER BY last_read_at DESC NULLS LAST LIMIT $1 ` @@ -3248,6 +3297,7 @@ type GetContinueSeriesItemsRow struct { 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"` @@ -3256,10 +3306,15 @@ type GetContinueSeriesItemsRow struct { 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"` + 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"` @@ -3329,6 +3384,7 @@ func (q *Queries) GetContinueSeriesItems(ctx context.Context, arg GetContinueSer &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -3340,7 +3396,12 @@ func (q *Queries) GetContinueSeriesItems(ctx context.Context, arg GetContinueSer &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -4592,7 +4653,7 @@ func (q *Queries) GetLibraryFolders(ctx context.Context, libraryID pgtype.UUID) } const GetLibraryItems = `-- name: GetLibraryItems :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.abridged, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid) AND mi.archived_at IS NULL AND mi.missing_scan_count = 0 @@ -4612,6 +4673,7 @@ func (q *Queries) GetLibraryItems(ctx context.Context, libraryID pgtype.UUID) ([ &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -4623,7 +4685,12 @@ func (q *Queries) GetLibraryItems(ctx context.Context, libraryID pgtype.UUID) ([ &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -5386,7 +5453,7 @@ func (q *Queries) GetMediaHighlights(ctx context.Context, arg GetMediaHighlights } const GetMediaItem = `-- name: GetMediaItem :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, abridged, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE id = $1 +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE id = $1 ` func (q *Queries) GetMediaItem(ctx context.Context, id pgtype.UUID) (MediaItems, error) { @@ -5396,6 +5463,7 @@ func (q *Queries) GetMediaItem(ctx context.Context, id pgtype.UUID) (MediaItems, &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -5407,7 +5475,12 @@ func (q *Queries) GetMediaItem(ctx context.Context, id pgtype.UUID) (MediaItems, &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -5466,7 +5539,7 @@ func (q *Queries) GetMediaItem(ctx context.Context, id pgtype.UUID) (MediaItems, } const GetMediaItemByFilePath = `-- name: GetMediaItemByFilePath :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, abridged, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_path = $1 AND library_id = $2 +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_path = $1 AND library_id = $2 ` type GetMediaItemByFilePathParams struct { @@ -5481,6 +5554,7 @@ func (q *Queries) GetMediaItemByFilePath(ctx context.Context, arg GetMediaItemBy &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -5492,7 +5566,12 @@ func (q *Queries) GetMediaItemByFilePath(ctx context.Context, arg GetMediaItemBy &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -5551,7 +5630,7 @@ func (q *Queries) GetMediaItemByFilePath(ctx context.Context, arg GetMediaItemBy } const GetMediaItemByFilePathAnyLibrary = `-- name: GetMediaItemByFilePathAnyLibrary :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, abridged, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_path = $1 LIMIT 1 +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_path = $1 LIMIT 1 ` func (q *Queries) GetMediaItemByFilePathAnyLibrary(ctx context.Context, filePath string) (MediaItems, error) { @@ -5561,6 +5640,7 @@ func (q *Queries) GetMediaItemByFilePathAnyLibrary(ctx context.Context, filePath &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -5572,7 +5652,12 @@ func (q *Queries) GetMediaItemByFilePathAnyLibrary(ctx context.Context, filePath &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -5632,7 +5717,7 @@ func (q *Queries) GetMediaItemByFilePathAnyLibrary(ctx context.Context, filePath const GetMediaItemByFilePathForSync = `-- name: GetMediaItemByFilePathForSync :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, abridged, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_path = $1 +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_path = $1 ` // ============================================ @@ -5645,6 +5730,7 @@ func (q *Queries) GetMediaItemByFilePathForSync(ctx context.Context, filePath st &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -5656,7 +5742,12 @@ func (q *Queries) GetMediaItemByFilePathForSync(ctx context.Context, filePath st &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -5715,7 +5806,7 @@ func (q *Queries) GetMediaItemByFilePathForSync(ctx context.Context, filePath st } const GetMediaItemByKoboContentId = `-- name: GetMediaItemByKoboContentId :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, abridged, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE kobo_content_id = $1 +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE kobo_content_id = $1 ` func (q *Queries) GetMediaItemByKoboContentId(ctx context.Context, koboContentID pgtype.Text) (MediaItems, error) { @@ -5725,6 +5816,7 @@ func (q *Queries) GetMediaItemByKoboContentId(ctx context.Context, koboContentID &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -5736,7 +5828,12 @@ func (q *Queries) GetMediaItemByKoboContentId(ctx context.Context, koboContentID &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -5795,7 +5892,7 @@ func (q *Queries) GetMediaItemByKoboContentId(ctx context.Context, koboContentID } const GetMediaItemByLibraryAndPath = `-- name: GetMediaItemByLibraryAndPath :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, abridged, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE library_id = $1 AND file_path = $2 +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE library_id = $1 AND file_path = $2 ` type GetMediaItemByLibraryAndPathParams struct { @@ -5810,6 +5907,7 @@ func (q *Queries) GetMediaItemByLibraryAndPath(ctx context.Context, arg GetMedia &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -5821,7 +5919,12 @@ func (q *Queries) GetMediaItemByLibraryAndPath(ctx context.Context, arg GetMedia &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -5880,7 +5983,7 @@ func (q *Queries) GetMediaItemByLibraryAndPath(ctx context.Context, arg GetMedia } const GetMediaItemByOPFIdentifier = `-- name: GetMediaItemByOPFIdentifier :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, abridged, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE opf_identifier = $1 +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE opf_identifier = $1 ` // Get media item by OPF identifier @@ -5891,6 +5994,7 @@ func (q *Queries) GetMediaItemByOPFIdentifier(ctx context.Context, opfIdentifier &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -5902,7 +6006,12 @@ func (q *Queries) GetMediaItemByOPFIdentifier(ctx context.Context, opfIdentifier &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -5961,7 +6070,7 @@ func (q *Queries) GetMediaItemByOPFIdentifier(ctx context.Context, opfIdentifier } const GetMediaItemByOPFUUID = `-- name: GetMediaItemByOPFUUID :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, abridged, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE opf_uuid = $1 +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE opf_uuid = $1 ` // Get media item by OPF UUID @@ -5972,6 +6081,7 @@ func (q *Queries) GetMediaItemByOPFUUID(ctx context.Context, opfUuid pgtype.Text &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -5983,7 +6093,12 @@ func (q *Queries) GetMediaItemByOPFUUID(ctx context.Context, opfUuid pgtype.Text &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -6042,7 +6157,7 @@ func (q *Queries) GetMediaItemByOPFUUID(ctx context.Context, opfUuid pgtype.Text } const GetMediaItemBySHA256 = `-- name: GetMediaItemBySHA256 :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, abridged, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_sha256 = $1 +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_sha256 = $1 ` // Get media item by SHA-256 hash @@ -6053,6 +6168,7 @@ func (q *Queries) GetMediaItemBySHA256(ctx context.Context, fileSha256 pgtype.Te &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -6064,7 +6180,12 @@ func (q *Queries) GetMediaItemBySHA256(ctx context.Context, fileSha256 pgtype.Te &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -6123,7 +6244,7 @@ func (q *Queries) GetMediaItemBySHA256(ctx context.Context, fileSha256 pgtype.Te } const GetMediaItemBySHA256AndLibrary = `-- name: GetMediaItemBySHA256AndLibrary :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, abridged, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_sha256 = $1 AND library_id = $2 +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_sha256 = $1 AND library_id = $2 ` type GetMediaItemBySHA256AndLibraryParams struct { @@ -6139,6 +6260,7 @@ func (q *Queries) GetMediaItemBySHA256AndLibrary(ctx context.Context, arg GetMed &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -6150,7 +6272,12 @@ func (q *Queries) GetMediaItemBySHA256AndLibrary(ctx context.Context, arg GetMed &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -6324,7 +6451,7 @@ func (q *Queries) GetMediaItemUsageCounts(ctx context.Context, mediaItemID pgtyp } const GetMediaItemsBySHA256AndSameLibraryType = `-- name: GetMediaItemsBySHA256AndSameLibraryType :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.abridged, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, lt.name AS library_type_name +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, lt.name AS library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id @@ -6346,6 +6473,7 @@ type GetMediaItemsBySHA256AndSameLibraryTypeRow struct { 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"` @@ -6354,10 +6482,15 @@ type GetMediaItemsBySHA256AndSameLibraryTypeRow struct { 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"` + 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"` @@ -6432,6 +6565,7 @@ func (q *Queries) GetMediaItemsBySHA256AndSameLibraryType(ctx context.Context, a &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -6443,7 +6577,12 @@ func (q *Queries) GetMediaItemsBySHA256AndSameLibraryType(ctx context.Context, a &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -6746,7 +6885,7 @@ func (q *Queries) GetNextRetryTime(ctx context.Context) (interface{}, error) { } const GetNotStartedItems = `-- name: GetNotStartedItems :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.abridged, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid) AND mi.archived_at IS NULL AND mi.missing_scan_count = 0 @@ -6779,6 +6918,7 @@ func (q *Queries) GetNotStartedItems(ctx context.Context, arg GetNotStartedItems &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -6790,7 +6930,12 @@ func (q *Queries) GetNotStartedItems(ctx context.Context, arg GetNotStartedItems &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -7156,7 +7301,7 @@ func (q *Queries) GetReadingSpeed(ctx context.Context, arg GetReadingSpeedParams } const GetRecentlyAddedItems = `-- name: GetRecentlyAddedItems :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.abridged, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid) AND mi.archived_at IS NULL AND mi.missing_scan_count = 0 @@ -7182,6 +7327,7 @@ func (q *Queries) GetRecentlyAddedItems(ctx context.Context, arg GetRecentlyAdde &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -7193,7 +7339,12 @@ func (q *Queries) GetRecentlyAddedItems(ctx context.Context, arg GetRecentlyAdde &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -7259,7 +7410,7 @@ func (q *Queries) GetRecentlyAddedItems(ctx context.Context, arg GetRecentlyAdde } const GetRecentlyReadItems = `-- name: GetRecentlyReadItems :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.abridged, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi INNER JOIN ( SELECT DISTINCT ON (media_item_id) media_item_id, last_read_at FROM reading_progress @@ -7293,6 +7444,7 @@ func (q *Queries) GetRecentlyReadItems(ctx context.Context, arg GetRecentlyReadI &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -7304,7 +7456,12 @@ func (q *Queries) GetRecentlyReadItems(ctx context.Context, arg GetRecentlyReadI &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -7470,12 +7627,12 @@ func (q *Queries) GetSavedFilters(ctx context.Context, arg GetSavedFiltersParams } const GetSeriesBooks = `-- name: GetSeriesBooks :many -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, abridged, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE series = $1 AND library_id = $2 AND archived_at IS NULL AND missing_scan_count = 0 -ORDER BY series_number ASC NULLS LAST +ORDER BY series_number ASC NULLS LAST, title ASC ` type GetSeriesBooksParams struct { @@ -7499,6 +7656,7 @@ func (q *Queries) GetSeriesBooks(ctx context.Context, arg GetSeriesBooksParams) &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -7510,7 +7668,12 @@ func (q *Queries) GetSeriesBooks(ctx context.Context, arg GetSeriesBooksParams) &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -9209,7 +9372,7 @@ func (q *Queries) GetVisibleLibraryMediaCounts(ctx context.Context, userID pgtyp } const GetVisibleLiveMediaForRules = `-- name: GetVisibleLiveMediaForRules :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.abridged, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid) AND ($2::uuid[] IS NULL OR mi.library_id = ANY($2::uuid[])) AND mi.archived_at IS NULL @@ -9240,6 +9403,7 @@ func (q *Queries) GetVisibleLiveMediaForRules(ctx context.Context, arg GetVisibl &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -9251,7 +9415,12 @@ func (q *Queries) GetVisibleLiveMediaForRules(ctx context.Context, arg GetVisibl &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -9819,7 +9988,7 @@ func (q *Queries) ListDevicesByUser(ctx context.Context, userID pgtype.UUID) ([] } const ListHiddenMediaItems = `-- name: ListHiddenMediaItems :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.abridged, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name, +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name, twin.id AS match_id, COALESCE(twin.title, '') AS match_title, twin_library.name AS match_library_name FROM media_items mi @@ -9849,6 +10018,7 @@ type ListHiddenMediaItemsRow struct { 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"` @@ -9857,10 +10027,15 @@ type ListHiddenMediaItemsRow struct { 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"` + 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"` @@ -9940,6 +10115,7 @@ func (q *Queries) ListHiddenMediaItems(ctx context.Context) ([]ListHiddenMediaIt &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -9951,7 +10127,12 @@ func (q *Queries) ListHiddenMediaItems(ctx context.Context) ([]ListHiddenMediaIt &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -10071,7 +10252,7 @@ func (q *Queries) ListLibraries(ctx context.Context) ([]ListLibrariesRow, error) } const ListMediaItems = `-- name: ListMediaItems :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.abridged, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id @@ -10091,6 +10272,7 @@ type ListMediaItemsRow struct { 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"` @@ -10099,10 +10281,15 @@ type ListMediaItemsRow struct { 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"` + 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"` @@ -10173,6 +10360,7 @@ func (q *Queries) ListMediaItems(ctx context.Context, arg ListMediaItemsParams) &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -10184,7 +10372,12 @@ func (q *Queries) ListMediaItems(ctx context.Context, arg ListMediaItemsParams) &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -10252,7 +10445,7 @@ func (q *Queries) ListMediaItems(ctx context.Context, arg ListMediaItemsParams) } const ListMediaItemsByLibrary = `-- name: ListMediaItemsByLibrary :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.abridged, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id @@ -10266,6 +10459,7 @@ type ListMediaItemsByLibraryRow struct { 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"` @@ -10274,10 +10468,15 @@ type ListMediaItemsByLibraryRow struct { 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"` + 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"` @@ -10348,6 +10547,7 @@ func (q *Queries) ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype. &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -10359,7 +10559,12 @@ func (q *Queries) ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype. &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -10427,7 +10632,7 @@ func (q *Queries) ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype. } const ListMediaItemsByLibraryIncludingArchived = `-- name: ListMediaItemsByLibraryIncludingArchived :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.abridged, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id @@ -10439,6 +10644,7 @@ type ListMediaItemsByLibraryIncludingArchivedRow struct { 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"` @@ -10447,10 +10653,15 @@ type ListMediaItemsByLibraryIncludingArchivedRow struct { 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"` + 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"` @@ -10521,6 +10732,7 @@ func (q *Queries) ListMediaItemsByLibraryIncludingArchived(ctx context.Context, &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -10532,7 +10744,12 @@ func (q *Queries) ListMediaItemsByLibraryIncludingArchived(ctx context.Context, &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -10600,7 +10817,7 @@ func (q *Queries) ListMediaItemsByLibraryIncludingArchived(ctx context.Context, } const ListMediaItemsBySHA256AndLibrary = `-- name: ListMediaItemsBySHA256AndLibrary :many -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, abridged, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_sha256 = $1 AND library_id = $2 ORDER BY file_path +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_sha256 = $1 AND library_id = $2 ORDER BY file_path ` type ListMediaItemsBySHA256AndLibraryParams struct { @@ -10622,6 +10839,7 @@ func (q *Queries) ListMediaItemsBySHA256AndLibrary(ctx context.Context, arg List &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -10633,7 +10851,12 @@ func (q *Queries) ListMediaItemsBySHA256AndLibrary(ctx context.Context, arg List &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -10699,7 +10922,7 @@ func (q *Queries) ListMediaItemsBySHA256AndLibrary(ctx context.Context, arg List } const ListMediaItemsMissingHash = `-- name: ListMediaItemsMissingHash :many -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, abridged, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_sha256 IS NULL ORDER BY created_at +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_sha256 IS NULL ORDER BY created_at ` // List media items that have no stored SHA-256 (imported before hashing existed) @@ -10716,6 +10939,7 @@ func (q *Queries) ListMediaItemsMissingHash(ctx context.Context) ([]MediaItems, &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -10727,7 +10951,12 @@ func (q *Queries) ListMediaItemsMissingHash(ctx context.Context) ([]MediaItems, &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -10793,7 +11022,7 @@ func (q *Queries) ListMediaItemsMissingHash(ctx context.Context) ([]MediaItems, } const ListMediaItemsSorted = `-- name: ListMediaItemsSorted :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.abridged, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id @@ -10880,6 +11109,7 @@ type ListMediaItemsSortedRow struct { 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"` @@ -10888,10 +11118,15 @@ type ListMediaItemsSortedRow struct { 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"` + 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"` @@ -10967,6 +11202,7 @@ func (q *Queries) ListMediaItemsSorted(ctx context.Context, arg ListMediaItemsSo &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -10978,7 +11214,12 @@ func (q *Queries) ListMediaItemsSorted(ctx context.Context, arg ListMediaItemsSo &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -12400,7 +12641,7 @@ func (q *Queries) SearchLanguageValues(ctx context.Context, arg SearchLanguageVa } const SearchMediaItems = `-- name: SearchMediaItems :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.abridged, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id @@ -12440,6 +12681,7 @@ type SearchMediaItemsRow struct { 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"` @@ -12448,10 +12690,15 @@ type SearchMediaItemsRow struct { 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"` + 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"` @@ -12529,6 +12776,7 @@ func (q *Queries) SearchMediaItems(ctx context.Context, arg SearchMediaItemsPara &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -12540,7 +12788,12 @@ func (q *Queries) SearchMediaItems(ctx context.Context, arg SearchMediaItemsPara &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -12836,7 +13089,7 @@ type SearchMediaItemsUnifiedRow struct { 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"` + SeriesNumber pgtype.Float8 `db:"series_number" json:"series_number"` Tags []string `db:"tags" json:"tags"` Asin pgtype.Text `db:"asin" json:"asin"` DatePublished pgtype.Date `db:"date_published" json:"date_published"` @@ -14093,65 +14346,72 @@ func (q *Queries) UpdateMediaHighlightForSync(ctx context.Context, arg UpdateMed const UpdateMediaItem = `-- name: UpdateMediaItem :one UPDATE media_items SET title = $2, - author = $3, - isbn = $4, - description = $5, - cover_image_path = $6, - series = $7, - series_number = $8, - tags = $9, - tags_search = $10, - asin = $11, - date_published = $12, - publisher = $13, - contributors = $14, - contributors_search = $15, - language = $16, - edition = $17, - page_count = $18, - genre = $19, - copyright_year = $20, - goodreads_id = $21, - openlibrary_id = $22, - google_books_id = $23, - manga_type = $24, - reading_direction = $25, - series_count = $26, - volume = $27, - imprint = $28, - age_rating = $29, - web_url = $30, - metadata_notes = $31, - community_rating = $32, - story_arc = $33, - is_black_and_white = $34, - alternate_info = $35, - scan_information = $36, - summary = $37, - metadata_overrides = $38, - audio_metadata = $39, - narrators = $40, - narrators_search = $41, - abridged = $42, + subtitle = $3, + authors = $4, + authors_search = $5, + isbn = $6, + description = $7, + cover_image_path = $8, + series = $9, + series_number = $10, + tags = $11, + tags_search = $12, + asin = $13, + date_published = $14, + publishers = $15, + publishers_search = $16, + contributors = $17, + contributors_search = $18, + language = $19, + edition = $20, + page_count = $21, + genre = $22, + copyright_year = $23, + goodreads_id = $24, + openlibrary_id = $25, + google_books_id = $26, + manga_type = $27, + reading_direction = $28, + series_count = $29, + volume = $30, + imprint = $31, + age_rating = $32, + web_url = $33, + metadata_notes = $34, + community_rating = $35, + story_arc = $36, + is_black_and_white = $37, + alternate_info = $38, + scan_information = $39, + summary = $40, + metadata_overrides = $41, + audio_metadata = $42, + narrators = $43, + narrators_search = $44, + abridged = $45, + explicit = $46, updated_at = NOW() WHERE id = $1 -RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, abridged, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence +RETURNING id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence ` type UpdateMediaItemParams struct { ID pgtype.UUID `db:"id" json:"id"` Title string `db:"title" json:"title"` - Author pgtype.Text `db:"author" json:"author"` + Subtitle pgtype.Text `db:"subtitle" json:"subtitle"` + Authors []string `db:"authors" json:"authors"` + AuthorsSearch []string `db:"authors_search" json:"authors_search"` Isbn pgtype.Text `db:"isbn" json:"isbn"` Description pgtype.Text `db:"description" json:"description"` 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"` + SeriesNumber pgtype.Float8 `db:"series_number" json:"series_number"` Tags []string `db:"tags" json:"tags"` TagsSearch []string `db:"tags_search" json:"tags_search"` Asin pgtype.Text `db:"asin" json:"asin"` DatePublished pgtype.Date `db:"date_published" json:"date_published"` - Publisher pgtype.Text `db:"publisher" json:"publisher"` + Publishers []string `db:"publishers" json:"publishers"` + PublishersSearch []string `db:"publishers_search" json:"publishers_search"` Contributors []string `db:"contributors" json:"contributors"` ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` Language pgtype.Text `db:"language" json:"language"` @@ -14181,13 +14441,16 @@ type UpdateMediaItemParams struct { Narrators []string `db:"narrators" json:"narrators"` NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` Abridged bool `db:"abridged" json:"abridged"` + Explicit bool `db:"explicit" json:"explicit"` } func (q *Queries) UpdateMediaItem(ctx context.Context, arg UpdateMediaItemParams) (MediaItems, error) { row := q.db.QueryRow(ctx, UpdateMediaItem, arg.ID, arg.Title, - arg.Author, + arg.Subtitle, + arg.Authors, + arg.AuthorsSearch, arg.Isbn, arg.Description, arg.CoverImagePath, @@ -14197,7 +14460,8 @@ func (q *Queries) UpdateMediaItem(ctx context.Context, arg UpdateMediaItemParams arg.TagsSearch, arg.Asin, arg.DatePublished, - arg.Publisher, + arg.Publishers, + arg.PublishersSearch, arg.Contributors, arg.ContributorsSearch, arg.Language, @@ -14227,12 +14491,14 @@ func (q *Queries) UpdateMediaItem(ctx context.Context, arg UpdateMediaItemParams arg.Narrators, arg.NarratorsSearch, arg.Abridged, + arg.Explicit, ) var i MediaItems err := row.Scan( &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -14244,7 +14510,12 @@ func (q *Queries) UpdateMediaItem(ctx context.Context, arg UpdateMediaItemParams &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -14321,11 +14592,82 @@ type UpdateMediaItemAudioBookPayloadParams struct { ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"` } +type UpdateMediaItemAudioBookPayloadRow 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.Float8 `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"` + Language pgtype.Text `db:"language" json:"language"` + Edition pgtype.Text `db:"edition" json:"edition"` + PageCount pgtype.Int4 `db:"page_count" json:"page_count"` + Genre pgtype.Text `db:"genre" json:"genre"` + CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"` + GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"` + OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"` + GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"` + AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + ImportedAt pgtype.Timestamptz `db:"imported_at" json:"imported_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + FormatGroup string `db:"format_group" json:"format_group"` + FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"` + IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"` + HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"` + TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"` + ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"` + EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"` + RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"` + KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"` + KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"` + MangaType pgtype.Text `db:"manga_type" json:"manga_type"` + ReadingDirection pgtype.Text `db:"reading_direction" json:"reading_direction"` + SeriesCount pgtype.Int4 `db:"series_count" json:"series_count"` + Volume pgtype.Int4 `db:"volume" json:"volume"` + Imprint pgtype.Text `db:"imprint" json:"imprint"` + AgeRating pgtype.Text `db:"age_rating" json:"age_rating"` + WebUrl pgtype.Text `db:"web_url" json:"web_url"` + StoryArc pgtype.Text `db:"story_arc" json:"story_arc"` + IsBlackAndWhite pgtype.Bool `db:"is_black_and_white" json:"is_black_and_white"` + MetadataNotes pgtype.Text `db:"metadata_notes" json:"metadata_notes"` + CommunityRating pgtype.Float8 `db:"community_rating" json:"community_rating"` + AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` + ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` + Summary pgtype.Text `db:"summary" json:"summary"` + MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` + MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` + ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` + ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` + LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` + AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` + TagsSearch []string `db:"tags_search" json:"tags_search"` + ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` + NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` + FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` + OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"` + OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"` + HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"` +} + // Focused refresh for multi-file audiobooks (folder-of-tracks / zip // books): rescans recompute the track manifest and the aggregate facts // that follow from it. Editorial fields stay untouched — admins may have // edited title/narrators after ingest. -func (q *Queries) UpdateMediaItemAudioBookPayload(ctx context.Context, arg UpdateMediaItemAudioBookPayloadParams) (MediaItems, error) { +func (q *Queries) UpdateMediaItemAudioBookPayload(ctx context.Context, arg UpdateMediaItemAudioBookPayloadParams) (UpdateMediaItemAudioBookPayloadRow, error) { row := q.db.QueryRow(ctx, UpdateMediaItemAudioBookPayload, arg.ID, arg.FileSize, @@ -14333,7 +14675,7 @@ func (q *Queries) UpdateMediaItemAudioBookPayload(ctx context.Context, arg Updat arg.AudioMetadata, arg.ChapterCount, ) - var i MediaItems + var i UpdateMediaItemAudioBookPayloadRow err := row.Scan( &i.ID, &i.LibraryID, @@ -14411,7 +14753,7 @@ const UpdateMediaItemChapterMetadata = `-- name: UpdateMediaItemChapterMetadata UPDATE media_items SET chapter_metadata = $2, updated_at = NOW() WHERE id = $1 -RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, abridged, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence +RETURNING id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence ` type UpdateMediaItemChapterMetadataParams struct { @@ -14426,6 +14768,7 @@ func (q *Queries) UpdateMediaItemChapterMetadata(ctx context.Context, arg Update &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -14437,7 +14780,12 @@ func (q *Queries) UpdateMediaItemChapterMetadata(ctx context.Context, arg Update &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -14590,7 +14938,7 @@ SET hash_confidence = $5, updated_at = NOW() WHERE id = $1 -RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, abridged, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence +RETURNING id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence ` type UpdateMediaItemIdentifiersParams struct { @@ -14619,6 +14967,7 @@ func (q *Queries) UpdateMediaItemIdentifiers(ctx context.Context, arg UpdateMedi &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -14630,7 +14979,12 @@ func (q *Queries) UpdateMediaItemIdentifiers(ctx context.Context, arg UpdateMedi &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, @@ -14696,7 +15050,7 @@ SET entitlement_id = $2, kobo_metadata = $5, updated_at = NOW() WHERE id = $1 -RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, abridged, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence +RETURNING id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence ` type UpdateMediaItemKoboMetadataParams struct { @@ -14720,6 +15074,7 @@ func (q *Queries) UpdateMediaItemKoboMetadata(ctx context.Context, arg UpdateMed &i.ID, &i.LibraryID, &i.Title, + &i.Subtitle, &i.Author, &i.Isbn, &i.Description, @@ -14731,7 +15086,12 @@ func (q *Queries) UpdateMediaItemKoboMetadata(ctx context.Context, arg UpdateMed &i.SeriesNumber, &i.Tags, &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, &i.Abridged, + &i.Explicit, &i.Asin, &i.DatePublished, &i.Publisher, diff --git a/internal/database/queries/queries.sql b/internal/database/queries/queries.sql index 966335e0..626df7ac 100644 --- a/internal/database/queries/queries.sql +++ b/internal/database/queries/queries.sql @@ -152,8 +152,8 @@ GROUP BY l.id; -- narrators/narrators_search are NOT NULL DEFAULT '{}' (A2): callers MUST -- pass empty slices, never Go nil (an explicit NULL INSERT bypasses the -- column default and 23502s). The API create handler normalizes nil. -INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publisher, contributors, contributors_search, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, library_type_name, audio_metadata, narrators, narrators_search, abridged) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48) +INSERT INTO media_items (library_id, title, subtitle, authors, authors_search, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publishers, publishers_search, contributors, contributors_search, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, library_type_name, audio_metadata, narrators, narrators_search, abridged, explicit) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51, $52) ON CONFLICT (library_id, file_path) DO UPDATE SET updated_at = NOW() RETURNING *; @@ -164,8 +164,8 @@ RETURNING *; -- and a duplicate flag instead of pretending it created something. -- narrators NOT NULL: callers pass empty slices, never nil (see -- CreateMediaItem note). -INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publisher, contributors, contributors_search, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, library_type_name, audio_metadata, narrators, narrators_search, abridged) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48) +INSERT INTO media_items (library_id, title, subtitle, authors, authors_search, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publishers, publishers_search, contributors, contributors_search, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, library_type_name, audio_metadata, narrators, narrators_search, abridged, explicit) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51, $52) ON CONFLICT (library_id, file_path) DO NOTHING RETURNING *; @@ -282,46 +282,50 @@ ORDER BY -- name: UpdateMediaItem :one UPDATE media_items SET title = $2, - author = $3, - isbn = $4, - description = $5, - cover_image_path = $6, - series = $7, - series_number = $8, - tags = $9, - tags_search = $10, - asin = $11, - date_published = $12, - publisher = $13, - contributors = $14, - contributors_search = $15, - language = $16, - edition = $17, - page_count = $18, - genre = $19, - copyright_year = $20, - goodreads_id = $21, - openlibrary_id = $22, - google_books_id = $23, - manga_type = $24, - reading_direction = $25, - series_count = $26, - volume = $27, - imprint = $28, - age_rating = $29, - web_url = $30, - metadata_notes = $31, - community_rating = $32, - story_arc = $33, - is_black_and_white = $34, - alternate_info = $35, - scan_information = $36, - summary = $37, - metadata_overrides = $38, - audio_metadata = $39, - narrators = $40, - narrators_search = $41, - abridged = $42, + subtitle = $3, + authors = $4, + authors_search = $5, + isbn = $6, + description = $7, + cover_image_path = $8, + series = $9, + series_number = $10, + tags = $11, + tags_search = $12, + asin = $13, + date_published = $14, + publishers = $15, + publishers_search = $16, + contributors = $17, + contributors_search = $18, + language = $19, + edition = $20, + page_count = $21, + genre = $22, + copyright_year = $23, + goodreads_id = $24, + openlibrary_id = $25, + google_books_id = $26, + manga_type = $27, + reading_direction = $28, + series_count = $29, + volume = $30, + imprint = $31, + age_rating = $32, + web_url = $33, + metadata_notes = $34, + community_rating = $35, + story_arc = $36, + is_black_and_white = $37, + alternate_info = $38, + scan_information = $39, + summary = $40, + metadata_overrides = $41, + audio_metadata = $42, + narrators = $43, + narrators_search = $44, + abridged = $45, + explicit = $46, updated_at = NOW() WHERE id = $1 RETURNING *; @@ -2917,7 +2921,7 @@ WHERE series = sqlc.narg('series') AND library_id = sqlc.narg('library_id') AND archived_at IS NULL AND missing_scan_count = 0 -ORDER BY series_number ASC NULLS LAST; +ORDER BY series_number ASC NULLS LAST, title ASC; -- name: GetContinueSeriesItems :many WITH user_series_progress AS ( diff --git a/internal/handlers/media.go b/internal/handlers/media.go index 2fc46495..42e6e0a2 100644 --- a/internal/handlers/media.go +++ b/internal/handlers/media.go @@ -39,10 +39,14 @@ type CreateMediaItemRequest struct { MimeType string `json:"mime_type" validate:"required"` CoverImagePath string `json:"cover_image_path"` Series string `json:"series"` - SeriesNumber int32 `json:"series_number"` + SeriesNumber float64 `json:"series_number"` Tags []string `json:"tags"` ASIN string `json:"asin"` DatePublished string `json:"date_published"` + Subtitle string `json:"subtitle"` + Explicit bool `json:"explicit"` + Authors []string `json:"authors"` // write truth; legacy `author` still accepted + Publishers []string `json:"publishers"` // write truth; legacy `publisher` still accepted Publisher string `json:"publisher"` Contributors []string `json:"contributors"` // NEW: Reading direction and comic metadata fields @@ -69,16 +73,20 @@ type CreateMediaItemRequest struct { type UpdateMediaItemRequest struct { Title string `form:"title" json:"title" validate:"required,min=1,max=500"` Author string `form:"author" json:"author"` + Authors []string `form:"authors" json:"authors"` // write truth; legacy `author` still accepted + Subtitle string `form:"subtitle" json:"subtitle"` + Explicit bool `form:"explicit" json:"explicit"` ISBN string `form:"isbn" json:"isbn"` Description string `form:"description" json:"description"` CoverImagePath string `form:"cover_image_path" json:"cover_image_path"` CoverAction string `form:"cover_action" json:"cover_action"` Series string `form:"series" json:"series"` - SeriesNumber int32 `form:"series_number" json:"series_number"` + SeriesNumber float64 `form:"series_number" json:"series_number"` Tags []string `form:"tags" json:"tags"` ASIN string `form:"asin" json:"asin"` DatePublished string `form:"date_published" json:"date_published"` Publisher string `form:"publisher" json:"publisher"` + Publishers []string `form:"publishers" json:"publishers"` // write truth; legacy `publisher` still accepted Contributors []string `form:"contributors" json:"contributors"` Language string `form:"language" json:"language"` Edition string `form:"edition" json:"edition"` @@ -588,13 +596,16 @@ func (h *MediaHandler) HandleBulkUpdate(c *echo.Context) error { updateParams := database.UpdateMediaItemParams{ ID: mediaUUID, Title: existingMedia.Title, - Author: existingMedia.Author, + Subtitle: existingMedia.Subtitle, + Authors: existingMedia.Authors, + AuthorsSearch: existingMedia.AuthorsSearch, Genre: existingMedia.Genre, Language: existingMedia.Language, Tags: existingMedia.Tags, TagsSearch: existingMedia.TagsSearch, Description: existingMedia.Description, - Publisher: existingMedia.Publisher, + Publishers: existingMedia.Publishers, + PublishersSearch: existingMedia.PublishersSearch, CopyrightYear: existingMedia.CopyrightYear, Isbn: existingMedia.Isbn, Series: existingMedia.Series, @@ -623,6 +634,7 @@ func (h *MediaHandler) HandleBulkUpdate(c *echo.Context) error { AlternateInfo: existingMedia.AlternateInfo, ScanInformation: existingMedia.ScanInformation, Summary: existingMedia.Summary, + Explicit: existingMedia.Explicit, } // Bulk edits are user customizations: keep existing overrides and mark @@ -633,7 +645,8 @@ func (h *MediaHandler) HandleBulkUpdate(c *echo.Context) error { overrides = utils.MergeOverrides(overrides, utils.OverrideTitle) } if update.Updates.Author != nil { - updateParams.Author = pgtype.Text{String: *update.Updates.Author, Valid: true} + updateParams.Authors = []string{*update.Updates.Author} + updateParams.AuthorsSearch = utils.NormalizeTagsSearch(updateParams.Authors) overrides = utils.MergeOverrides(overrides, utils.OverrideAuthor) } if update.Updates.Genre != nil { @@ -1415,7 +1428,9 @@ func (mh *MediaHandler) CreateMediaItem(c *echo.Context) error { created, err := mh.db.CreateMediaItemIfNew(c.Request().Context(), database.CreateMediaItemIfNewParams{ LibraryID: pgtype.UUID{Bytes: req.LibraryID, Valid: true}, Title: req.Title, - Author: pgtype.Text{String: req.Author, Valid: req.Author != ""}, + Subtitle: pgtype.Text{String: req.Subtitle, Valid: req.Subtitle != ""}, + Authors: utils.SingleOrSlice(req.Author, req.Authors), + AuthorsSearch: utils.NormalizeTagsSearch(utils.SingleOrSlice(req.Author, req.Authors)), Isbn: pgtype.Text{String: isbnValue, Valid: req.ISBN != ""}, Description: pgtype.Text{String: req.Description, Valid: req.Description != ""}, FilePath: req.FilePath, @@ -1423,15 +1438,17 @@ func (mh *MediaHandler) CreateMediaItem(c *echo.Context) error { MimeType: pgtype.Text{String: req.MimeType, Valid: req.MimeType != ""}, CoverImagePath: pgtype.Text{String: req.CoverImagePath, Valid: req.CoverImagePath != ""}, Series: pgtype.Text{String: req.Series, Valid: req.Series != ""}, - SeriesNumber: pgtype.Int4{Int32: req.SeriesNumber, Valid: req.SeriesNumber > 0}, + SeriesNumber: pgtype.Float8{Float64: req.SeriesNumber, Valid: req.SeriesNumber > 0}, Tags: req.Tags, TagsSearch: tagsSearch, Narrators: narrators, NarratorsSearch: narratorsSearch, Abridged: req.Abridged, + Explicit: req.Explicit, Asin: pgtype.Text{String: req.ASIN, Valid: req.ASIN != ""}, DatePublished: pgtype.Date{Time: parseDate(req.DatePublished), Valid: req.DatePublished != ""}, - Publisher: pgtype.Text{String: req.Publisher, Valid: req.Publisher != ""}, + Publishers: utils.SingleOrSlice(req.Publisher, req.Publishers), + PublishersSearch: utils.NormalizeTagsSearch(utils.SingleOrSlice(req.Publisher, req.Publishers)), Contributors: req.Contributors, ContributorsSearch: contributorsSearch, AddedByAdminID: user.ID, @@ -1541,20 +1558,24 @@ func (mh *MediaHandler) UpdateMediaItem(c *echo.Context) error { params := database.UpdateMediaItemParams{ ID: pgtype.UUID{Bytes: mediaUUID, Valid: true}, Title: req.Title, - Author: pgtype.Text{String: req.Author, Valid: req.Author != ""}, + Subtitle: pgtype.Text{String: req.Subtitle, Valid: req.Subtitle != ""}, + Authors: utils.SingleOrSlice(req.Author, req.Authors), + AuthorsSearch: utils.NormalizeTagsSearch(utils.SingleOrSlice(req.Author, req.Authors)), Isbn: pgtype.Text{String: isbnValue, Valid: req.ISBN != ""}, Description: pgtype.Text{String: req.Description, Valid: req.Description != ""}, CoverImagePath: pgtype.Text{String: coverPath, Valid: coverPath != ""}, Series: pgtype.Text{String: req.Series, Valid: req.Series != ""}, - SeriesNumber: pgtype.Int4{Int32: req.SeriesNumber, Valid: req.SeriesNumber > 0}, + SeriesNumber: pgtype.Float8{Float64: req.SeriesNumber, Valid: req.SeriesNumber > 0}, Tags: req.Tags, TagsSearch: tagsSearch, Narrators: narrators, NarratorsSearch: narratorsSearch, Abridged: req.Abridged, + Explicit: req.Explicit, Asin: pgtype.Text{String: req.ASIN, Valid: req.ASIN != ""}, DatePublished: pgtype.Date{Time: parseDate(req.DatePublished), Valid: req.DatePublished != ""}, - Publisher: pgtype.Text{String: req.Publisher, Valid: req.Publisher != ""}, + Publishers: utils.SingleOrSlice(req.Publisher, req.Publishers), + PublishersSearch: utils.NormalizeTagsSearch(utils.SingleOrSlice(req.Publisher, req.Publishers)), Contributors: req.Contributors, ContributorsSearch: contributorsSearch, Language: pgtype.Text{String: req.Language, Valid: req.Language != ""}, diff --git a/internal/services/media_scanner.go b/internal/services/media_scanner.go index f4cab34b..b24932fc 100644 --- a/internal/services/media_scanner.go +++ b/internal/services/media_scanner.go @@ -49,11 +49,14 @@ import ( // MediaMetadata contains extracted metadata for media files (ebooks, comics, manga) type MediaMetadata struct { Title string - Author string + Subtitle string + Author string // legacy single-value fill; Authors[] is the write truth + Authors []string Description string Series string - SeriesNumber int32 - Publisher string + SeriesNumber float64 + Publisher string // legacy single-value fill; Publishers[] is the write truth + Publishers []string PublishDate time.Time Contributors []string CoverPath string @@ -61,6 +64,7 @@ type MediaMetadata struct { ASIN string Tags []string Genre string + Explicit bool FileHashInfo *HashInfo FileFormats []*FormatInfo @@ -715,8 +719,8 @@ func (s *MediaScanner) extractFolderStructureMetadata(path, rootFolder string) * seriesMatch := regexp.MustCompile(`^(.*),\s+Book\s+#(\d+)\s*-\s*(.*)$`).FindStringSubmatch(possibleSeries) if len(seriesMatch) == 4 { metadata.Series = strings.ReplaceAll(seriesMatch[1], "_", " ") - if seriesNum, err := strconv.ParseInt(seriesMatch[2], 10, 32); err == nil { - metadata.SeriesNumber = int32(seriesNum) + if seriesNum, err := strconv.ParseFloat(seriesMatch[2], 64); err == nil { + metadata.SeriesNumber = seriesNum } metadata.Title = strings.ReplaceAll(possibleTitle, "_", " ") } else { @@ -725,11 +729,11 @@ func (s *MediaScanner) extractFolderStructureMetadata(path, rootFolder string) * metadata.Title = strings.ReplaceAll(possibleTitle, "_", " ") // Try to extract series number from title - titleNumMatch := regexp.MustCompile(`^(.*)\s+(\d+)$`).FindStringSubmatch(metadata.Title) + titleNumMatch := regexp.MustCompile(`^(.*)\s+(\d+(?:\.\d+)?)$`).FindStringSubmatch(metadata.Title) if len(titleNumMatch) == 3 { metadata.Title = titleNumMatch[1] - if seriesNum, err := strconv.ParseInt(titleNumMatch[2], 10, 32); err == nil { - metadata.SeriesNumber = int32(seriesNum) + if seriesNum, err := strconv.ParseFloat(titleNumMatch[2], 64); err == nil { + metadata.SeriesNumber = seriesNum } } } @@ -1009,7 +1013,9 @@ func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool, createdItem, err := s.db.CreateMediaItem(ctx, database.CreateMediaItemParams{ LibraryID: libraryID, Title: metadata.Title, - Author: pgtype.Text{String: metadata.Author, Valid: metadata.Author != ""}, + Subtitle: pgtype.Text{String: metadata.Subtitle, Valid: metadata.Subtitle != ""}, + Authors: nilToEmpty(utils.SingleOrSlice(metadata.Author, metadata.Authors)), + AuthorsSearch: utils.NormalizeTagsSearch(utils.SingleOrSlice(metadata.Author, metadata.Authors)), Isbn: pgtype.Text{String: utils.NormalizeISBNSafe(metadata.ISBN), Valid: metadata.ISBN != ""}, Asin: pgtype.Text{String: metadata.ASIN, Valid: metadata.ASIN != ""}, Description: pgtype.Text{String: metadata.Description, Valid: metadata.Description != ""}, @@ -1018,8 +1024,9 @@ func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool, MimeType: pgtype.Text{String: s.getMimeType(path), Valid: true}, CoverImagePath: pgtype.Text{String: metadata.CoverPath, Valid: metadata.CoverPath != ""}, Series: pgtype.Text{String: metadata.Series, Valid: metadata.Series != ""}, - SeriesNumber: pgtype.Int4{Int32: metadata.SeriesNumber, Valid: metadata.SeriesNumber > 0}, - Publisher: pgtype.Text{String: metadata.Publisher, Valid: metadata.Publisher != ""}, + SeriesNumber: pgtype.Float8{Float64: metadata.SeriesNumber, Valid: metadata.SeriesNumber > 0}, + Publishers: nilToEmpty(utils.SingleOrSlice(metadata.Publisher, metadata.Publishers)), + PublishersSearch: utils.NormalizeTagsSearch(utils.SingleOrSlice(metadata.Publisher, metadata.Publishers)), DatePublished: pgtype.Date{Time: metadata.PublishDate, Valid: !metadata.PublishDate.IsZero()}, Genre: pgtype.Text{String: metadata.Genre, Valid: metadata.Genre != ""}, Contributors: metadata.Contributors, @@ -1027,6 +1034,7 @@ func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool, Narrators: nilToEmpty(metadata.Narrators), NarratorsSearch: utils.NormalizeNarratorsSearch(metadata.Narrators), Abridged: metadata.Abridged, + Explicit: metadata.Explicit, Tags: metadata.Tags, TagsSearch: tagsSearch, AddedByAdminID: s.adminID, @@ -1240,8 +1248,8 @@ func extractAudiobookshelfSidecar(path string) *MediaMetadata { } if seriesName != "" { metadata.Series = strings.TrimSpace(seriesName) - if index, err := strconv.ParseFloat(strings.TrimSpace(seriesSequence), 32); err == nil { - metadata.SeriesNumber = int32(index) + if index, err := strconv.ParseFloat(strings.TrimSpace(seriesSequence), 64); err == nil { + metadata.SeriesNumber = index } } if tags := append(append([]string{}, sidecar.Genres...), sidecar.Tags...); len(tags) > 0 { @@ -1400,7 +1408,7 @@ func (s *MediaScanner) mergeMetadata(path string, calibreMetadata *MediaMetadata metadata.Series = comicInfo.Series } if metadata.SeriesNumber == 0 && comicInfo.Number > 0 { - metadata.SeriesNumber = int32(comicInfo.Number) + metadata.SeriesNumber = float64(comicInfo.Number) } if metadata.Publisher == "" && comicInfo.Publisher != "" { metadata.Publisher = comicInfo.Publisher @@ -2221,7 +2229,7 @@ func parseOPFContent(content []byte) (*MediaMetadata, error) { if series, index := opf.readSeries(); series != "" { metadata.Series = series if index > 0 { - metadata.SeriesNumber = int32(index) + metadata.SeriesNumber = index } } // Calibre-specific meta tags (filter by name attribute) @@ -3329,20 +3337,24 @@ func (s *MediaScanner) updateMediaItemWithMetadata(ctx context.Context, existing params := database.UpdateMediaItemParams{ ID: existing.ID, Title: metadata.Title, - Author: pgtype.Text{String: metadata.Author, Valid: metadata.Author != ""}, + Subtitle: pgtype.Text{String: metadata.Subtitle, Valid: metadata.Subtitle != ""}, + Authors: nilToEmpty(utils.SingleOrSlice(metadata.Author, metadata.Authors)), + AuthorsSearch: utils.NormalizeTagsSearch(utils.SingleOrSlice(metadata.Author, metadata.Authors)), Isbn: pgtype.Text{String: utils.NormalizeISBNSafe(metadata.ISBN), Valid: metadata.ISBN != ""}, Description: pgtype.Text{String: metadata.Description, Valid: metadata.Description != ""}, CoverImagePath: pgtype.Text{String: s.getRelativePath(metadata.CoverPath), Valid: metadata.CoverPath != ""}, Series: pgtype.Text{String: metadata.Series, Valid: metadata.Series != ""}, - SeriesNumber: pgtype.Int4{Int32: metadata.SeriesNumber, Valid: metadata.SeriesNumber > 0}, + SeriesNumber: pgtype.Float8{Float64: metadata.SeriesNumber, Valid: metadata.SeriesNumber > 0}, Narrators: nilToEmpty(metadata.Narrators), NarratorsSearch: utils.NormalizeNarratorsSearch(metadata.Narrators), Abridged: metadata.Abridged, + Explicit: metadata.Explicit, Tags: metadata.Tags, TagsSearch: tagsSearch, Asin: pgtype.Text{String: metadata.ASIN, Valid: metadata.ASIN != ""}, DatePublished: pgtype.Date{Time: metadata.PublishDate, Valid: !metadata.PublishDate.IsZero()}, - Publisher: pgtype.Text{String: metadata.Publisher, Valid: metadata.Publisher != ""}, + Publishers: nilToEmpty(utils.SingleOrSlice(metadata.Publisher, metadata.Publishers)), + PublishersSearch: utils.NormalizeTagsSearch(utils.SingleOrSlice(metadata.Publisher, metadata.Publishers)), Contributors: metadata.Contributors, ContributorsSearch: contributorsSearch, Language: pgtype.Text{String: metadata.Language, Valid: metadata.Language != ""}, diff --git a/internal/services/media_scanner_audio.go b/internal/services/media_scanner_audio.go index bf89ab55..98cce1c2 100644 --- a/internal/services/media_scanner_audio.go +++ b/internal/services/media_scanner_audio.go @@ -437,8 +437,8 @@ func applyAudioTagFallbacks(metadata *MediaMetadata, report *ffprobeReport) { if series := report.tag("grouping", "----:com.apple.itunes:series"); series != "" { metadata.Series = series if part := report.tag("----:com.apple.itunes:series-part"); part != "" { - if idx, err := strconv.ParseFloat(strings.TrimSpace(part), 32); err == nil { - metadata.SeriesNumber = int32(idx) + if idx, err := strconv.ParseFloat(strings.TrimSpace(part), 64); err == nil { + metadata.SeriesNumber = idx } } } diff --git a/internal/services/media_scanner_audio_metadata_test.go b/internal/services/media_scanner_audio_metadata_test.go index bea14b8d..e2adf018 100644 --- a/internal/services/media_scanner_audio_metadata_test.go +++ b/internal/services/media_scanner_audio_metadata_test.go @@ -163,7 +163,7 @@ func TestApplyAudioTagFallbacks(t *testing.T) { t.Fatal("series not filled from tags") } if meta.SeriesNumber != 4 { - t.Fatalf("series number = %d", meta.SeriesNumber) + t.Fatalf("series number = %v", meta.SeriesNumber) } if meta.ASIN != "B0ASIN1234" { t.Fatalf("asin = %q", meta.ASIN) diff --git a/internal/services/media_scanner_audio_multifile.go b/internal/services/media_scanner_audio_multifile.go index 43630d11..6dab408e 100644 --- a/internal/services/media_scanner_audio_multifile.go +++ b/internal/services/media_scanner_audio_multifile.go @@ -536,7 +536,9 @@ func (s *MediaScanner) createAudioBookItem(ctx context.Context, libraryID pgtype return s.db.CreateMediaItem(ctx, database.CreateMediaItemParams{ LibraryID: libraryID, Title: metadata.Title, - Author: pgtype.Text{String: metadata.Author, Valid: metadata.Author != ""}, + Subtitle: pgtype.Text{String: metadata.Subtitle, Valid: metadata.Subtitle != ""}, + Authors: nilToEmpty(utils.SingleOrSlice(metadata.Author, metadata.Authors)), + AuthorsSearch: utils.NormalizeTagsSearch(utils.SingleOrSlice(metadata.Author, metadata.Authors)), Isbn: pgtype.Text{String: utils.NormalizeISBNSafe(metadata.ISBN), Valid: metadata.ISBN != ""}, Asin: pgtype.Text{String: metadata.ASIN, Valid: metadata.ASIN != ""}, Description: pgtype.Text{String: metadata.Description, Valid: metadata.Description != ""}, @@ -545,14 +547,16 @@ func (s *MediaScanner) createAudioBookItem(ctx context.Context, libraryID pgtype MimeType: pgtype.Text{String: mimeType, Valid: mimeType != ""}, CoverImagePath: pgtype.Text{String: metadata.CoverPath, Valid: metadata.CoverPath != ""}, Series: pgtype.Text{String: metadata.Series, Valid: metadata.Series != ""}, - SeriesNumber: pgtype.Int4{Int32: metadata.SeriesNumber, Valid: metadata.SeriesNumber > 0}, - Publisher: pgtype.Text{String: metadata.Publisher, Valid: metadata.Publisher != ""}, + SeriesNumber: pgtype.Float8{Float64: metadata.SeriesNumber, Valid: metadata.SeriesNumber > 0}, + Publishers: nilToEmpty(utils.SingleOrSlice(metadata.Publisher, metadata.Publishers)), + PublishersSearch: utils.NormalizeTagsSearch(utils.SingleOrSlice(metadata.Publisher, metadata.Publishers)), DatePublished: pgtype.Date{Time: metadata.PublishDate, Valid: !metadata.PublishDate.IsZero()}, Genre: pgtype.Text{String: metadata.Genre, Valid: metadata.Genre != ""}, Contributors: utils.NormalizeContributors(metadata.Contributors), Narrators: nilToEmpty(metadata.Narrators), NarratorsSearch: utils.NormalizeNarratorsSearch(metadata.Narrators), Abridged: metadata.Abridged, + Explicit: metadata.Explicit, Tags: utils.NormalizeTags(metadata.Tags), AddedByAdminID: s.adminID, CreatedAt: modTime, diff --git a/internal/services/media_scanner_metadata_test.go b/internal/services/media_scanner_metadata_test.go index 5902131d..aea9c8db 100644 --- a/internal/services/media_scanner_metadata_test.go +++ b/internal/services/media_scanner_metadata_test.go @@ -139,8 +139,8 @@ func TestParseOPFContentCalibreSeries(t *testing.T) { if err != nil { t.Fatalf("parseOPFContent() error: %v", err) } - if metadata.Series != "Great Series" || metadata.SeriesNumber != 2 { - t.Errorf("Series = %q/%d, want Great Series/2", metadata.Series, metadata.SeriesNumber) + if metadata.Series != "Great Series" || metadata.SeriesNumber != 2.5 { + t.Errorf("Series = %q/%v, want Great Series/2.5", metadata.Series, metadata.SeriesNumber) } if metadata.ISBN == "" { t.Error("schemed ISBN not extracted") @@ -179,8 +179,8 @@ func TestExtractAudiobookshelfSidecar(t *testing.T) { if m.Title != "An Book" || m.Author != "Author One" { t.Errorf("Title/Author = %q/%q", m.Title, m.Author) } - if m.Series != "The Series" || m.SeriesNumber != 4 { - t.Errorf("Series = %q/%d, want The Series/4", m.Series, m.SeriesNumber) + if m.Series != "The Series" || m.SeriesNumber != 4.5 { + t.Errorf("Series = %q/%v, want The Series/4.5", m.Series, m.SeriesNumber) } if len(m.Tags) != 2 { t.Errorf("Tags = %v, want genres+tags merged", m.Tags) diff --git a/internal/services/media_scanner_opf_test.go b/internal/services/media_scanner_opf_test.go index 7117eeb4..2f216513 100644 --- a/internal/services/media_scanner_opf_test.go +++ b/internal/services/media_scanner_opf_test.go @@ -171,8 +171,8 @@ func TestParseOPFContentTitleTypeAndSeries(t *testing.T) { if want := "The Main Title: The Subtitle"; metadata.Title != want { t.Errorf("Title = %q, want %q", metadata.Title, want) } - if metadata.Series != "Great Series" || metadata.SeriesNumber != 4 { - t.Errorf("Series = %q/%d, want Great Series/4", metadata.Series, metadata.SeriesNumber) + if metadata.Series != "Great Series" || metadata.SeriesNumber != 4.5 { + t.Errorf("Series = %q/%v, want Great Series/4.5", metadata.Series, metadata.SeriesNumber) } if metadata.ISBN == "" { t.Error("urn:isbn: identifier not extracted") diff --git a/internal/services/series_service_test.go b/internal/services/series_service_test.go index 2907f34b..b3e58a6a 100644 --- a/internal/services/series_service_test.go +++ b/internal/services/series_service_test.go @@ -26,7 +26,7 @@ func TestContinueSeriesRowToMediaItems_FieldsMappedCorrectly(t *testing.T) { 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.Int4{Int32: 3, Valid: true}, + SeriesNumber: pgtype.Float8{Float64: 3, Valid: true}, Tags: []string{"fantasy", "adventure"}, FormatGroup: "epub", AddedByAdminID: pgtype.UUID{Bytes: adminUUID, Valid: true}, @@ -43,7 +43,7 @@ func TestContinueSeriesRowToMediaItems_FieldsMappedCorrectly(t *testing.T) { 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.Int4{Int32: 3, Valid: true}, result.SeriesNumber, "SeriesNumber 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") } @@ -64,7 +64,7 @@ func TestContinueSeriesRowToMediaItems_NullFieldsHandled(t *testing.T) { MimeType: pgtype.Text{Valid: false}, CoverImagePath: pgtype.Text{Valid: false}, Series: pgtype.Text{Valid: false}, - SeriesNumber: pgtype.Int4{Valid: false}, + SeriesNumber: pgtype.Float8{Valid: false}, Tags: nil, FormatGroup: "epub", } @@ -94,7 +94,7 @@ func TestContinueSeriesRowToMediaItems_AllFieldsMapped(t *testing.T) { MimeType: pgtype.Text{String: "epub", Valid: true}, CoverImagePath: pgtype.Text{String: "/cover.jpg", Valid: true}, Series: pgtype.Text{String: "Series", Valid: true}, - SeriesNumber: pgtype.Int4{Int32: 1, 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}, diff --git a/internal/utils/metadata_overrides.go b/internal/utils/metadata_overrides.go index 4d3b0bf4..67087a90 100644 --- a/internal/utils/metadata_overrides.go +++ b/internal/utils/metadata_overrides.go @@ -18,7 +18,7 @@ import ( const ( OverrideTitle = "title" - OverrideAuthor = "author" + OverrideAuthor = "author" // protects the authors[] array (legacy key preserved) OverrideISBN = "isbn" OverrideDescription = "description" OverrideCoverImagePath = "cover_image_path" @@ -29,7 +29,9 @@ const ( OverrideTags = "tags" OverrideAsin = "asin" OverrideDatePublished = "date_published" - OverridePublisher = "publisher" + OverrideSubtitle = "subtitle" + OverrideExplicit = "explicit" + OverridePublisher = "publisher" // protects the publishers[] array (legacy key preserved) OverrideContributors = "contributors" OverrideLanguage = "language" OverrideEdition = "edition" @@ -150,17 +152,19 @@ func detectMetadataOverridesDiff(params database.UpdateMediaItemParams, existing } add(OverrideTitle, params.Title != existing.Title) - add(OverrideAuthor, normText(params.Author) != normText(existing.Author)) + add(OverrideSubtitle, normText(params.Subtitle) != normText(existing.Subtitle)) + add(OverrideAuthor, normStringSlice(params.Authors) != normStringSlice(existing.Authors)) add(OverrideISBN, normText(params.Isbn) != normText(existing.Isbn)) add(OverrideDescription, normText(params.Description) != normText(existing.Description)) add(OverrideSeries, normText(params.Series) != normText(existing.Series)) - add(OverrideSeriesNumber, normInt(params.SeriesNumber) != normInt(existing.SeriesNumber)) + add(OverrideSeriesNumber, normFloat(params.SeriesNumber) != normFloat(existing.SeriesNumber)) add(OverrideTags, normStringSlice(params.Tags) != normStringSlice(existing.Tags)) add(OverrideNarrators, normStringSlice(params.Narrators) != normStringSlice(existing.Narrators)) add(OverrideAbridged, params.Abridged != existing.Abridged) + add(OverrideExplicit, params.Explicit != existing.Explicit) add(OverrideAsin, normText(params.Asin) != normText(existing.Asin)) add(OverrideDatePublished, normDate(params.DatePublished) != normDate(existing.DatePublished)) - add(OverridePublisher, normText(params.Publisher) != normText(existing.Publisher)) + add(OverridePublisher, normStringSlice(params.Publishers) != normStringSlice(existing.Publishers)) add(OverrideContributors, normStringSlice(params.Contributors) != normStringSlice(existing.Contributors)) add(OverrideLanguage, normText(params.Language) != normText(existing.Language)) add(OverrideEdition, normText(params.Edition) != normText(existing.Edition)) @@ -205,8 +209,12 @@ func ApplyMetadataOverrides(params *database.UpdateMediaItemParams, existing dat if hasOverride(o, OverrideTitle) { params.Title = existing.Title } + if hasOverride(o, OverrideSubtitle) { + params.Subtitle = existing.Subtitle + } if hasOverride(o, OverrideAuthor) { - params.Author = existing.Author + params.Authors = existing.Authors + params.AuthorsSearch = existing.AuthorsSearch } if hasOverride(o, OverrideISBN) { params.Isbn = existing.Isbn @@ -241,7 +249,8 @@ func ApplyMetadataOverrides(params *database.UpdateMediaItemParams, existing dat params.DatePublished = existing.DatePublished } if hasOverride(o, OverridePublisher) { - params.Publisher = existing.Publisher + params.Publishers = existing.Publishers + params.PublishersSearch = existing.PublishersSearch } if hasOverride(o, OverrideContributors) { params.Contributors = existing.Contributors diff --git a/internal/utils/metadata_overrides_test.go b/internal/utils/metadata_overrides_test.go index 95c8145e..40a4baf6 100644 --- a/internal/utils/metadata_overrides_test.go +++ b/internal/utils/metadata_overrides_test.go @@ -63,12 +63,12 @@ func TestDetectMetadataOverridesUnsetFormsEqual(t *testing.T) { // Zero-value params must not look "changed" against NULL-ish columns. existing := database.MediaItems{ PageCount: pgtype.Int4{Int32: 0, Valid: false}, - SeriesNumber: pgtype.Int4{Int32: 5, Valid: true}, + SeriesNumber: pgtype.Float8{Float64: 5, Valid: true}, CommunityRating: pgtype.Float8{Float64: 0, Valid: false}, } params := database.UpdateMediaItemParams{ PageCount: pgtype.Int4{Int32: 0, Valid: false}, - SeriesNumber: pgtype.Int4{Int32: 0, Valid: false}, // cleared by user -> changed + SeriesNumber: pgtype.Float8{Float64: 0, Valid: false}, // cleared by user -> changed CommunityRating: pgtype.Float8{Float64: 0, Valid: false}, } got := DetectMetadataOverrides(params, existing) diff --git a/internal/utils/tags.go b/internal/utils/tags.go index 3e83b59e..c9ca00a6 100644 --- a/internal/utils/tags.go +++ b/internal/utils/tags.go @@ -183,6 +183,21 @@ func NormalizeNarratorsSearch(narrators []string) []string { return NormalizeTagsSearch(narrators) } +// SingleOrSlice returns the write-truth array for a field that has both a +// legacy single-value form and an array form: the explicit array when +// populated, else the single string as a one-element slice. Used by the +// authors/publishers arrays (`author`/`publisher` varchars regenerate from +// them via the sync_author_publisher_display trigger). +func SingleOrSlice(single string, arr []string) []string { + if len(arr) > 0 { + return arr + } + if single == "" { + return []string{} + } + return []string{single} +} + // NormalizeContributors normalizes an array of contributors for display: // 1. Trim whitespace // 2. Preserve original casing (including CAPSLOCK companies) diff --git a/templates/book_detail.templ b/templates/book_detail.templ index 30fbbb48..cbe4f87e 100644 --- a/templates/book_detail.templ +++ b/templates/book_detail.templ @@ -191,8 +191,8 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string, tomb style="background-color: var(--accent); color: var(--bg-primary); text-decoration: none;" > { book.Series.String } - if book.SeriesNumber.Valid && book.SeriesNumber.Int32 > 0 { - #{ book.SeriesNumber.Int32 } + if book.SeriesNumber.Valid && book.SeriesNumber.Float64 > 0 { + #{ fmtNumber(book.SeriesNumber.Float64) } } } diff --git a/templates/book_detail_modals.templ b/templates/book_detail_modals.templ index a01a70dd..1d6cd5b2 100644 --- a/templates/book_detail_modals.templ +++ b/templates/book_detail_modals.templ @@ -447,7 +447,7 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
diff --git a/templates/book_detail_modals_templ.go b/templates/book_detail_modals_templ.go index 32bb02fc..c71bd8b8 100644 --- a/templates/book_detail_modals_templ.go +++ b/templates/book_detail_modals_templ.go @@ -963,9 +963,9 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var48 string - templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", book.SeriesNumber.Int32)) + templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmtNumber(book.SeriesNumber.Float64)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 450, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 450, Col: 54} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var48) if templ_7745c5c3_Err != nil { diff --git a/templates/book_detail_templ.go b/templates/book_detail_templ.go index 7a227e51..ce1d964d 100644 --- a/templates/book_detail_templ.go +++ b/templates/book_detail_templ.go @@ -537,15 +537,15 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string, tombs if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if book.SeriesNumber.Valid && book.SeriesNumber.Int32 > 0 { + if book.SeriesNumber.Valid && book.SeriesNumber.Float64 > 0 { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "#") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var26 string - templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(book.SeriesNumber.Int32) + templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(fmtNumber(book.SeriesNumber.Float64)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 195, Col: 37} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 195, Col: 50} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) if templ_7745c5c3_Err != nil { diff --git a/templates/utils.go b/templates/utils.go index f32e7649..ece0c858 100644 --- a/templates/utils.go +++ b/templates/utils.go @@ -5,6 +5,7 @@ import ( "bookhoard/internal/handlers" "encoding/json" "fmt" + "strconv" "net/url" "sort" "strings" @@ -467,3 +468,10 @@ func LibraryTypeDisplayName(name string) string { r := []rune(name) return strings.ToUpper(string(r[0])) + string(r[1:]) } + +// fmtNumber renders a series ordinal without a trailing decimal zero: +// 1 → "1", 1.5 → "1.5". Series numbers became double precision when +// mid-series novellas (0.5/1.5) arrived; "%d" no longer fits. +func fmtNumber(f float64) string { + return strconv.FormatFloat(f, 'f', -1, 64) +}