diff --git a/database/schema/schema.sql b/database/schema/schema.sql index d775742d..948dc425 100644 --- a/database/schema/schema.sql +++ b/database/schema/schema.sql @@ -146,6 +146,8 @@ CREATE TABLE IF NOT EXISTS media_items ( series VARCHAR(255), series_number INTEGER, tags TEXT[], + narrators TEXT[], -- Audiobook narrators (sidecar first; user-editable) + abridged BOOLEAN NOT NULL DEFAULT FALSE, -- Libation/Audible abridged flag asin VARCHAR(20), -- Still relevant for ebooks date_published DATE, publisher VARCHAR(255), @@ -255,6 +257,13 @@ CREATE INDEX IF NOT EXISTS idx_media_items_contributors_gin ON media_items USING ALTER TABLE media_items ADD COLUMN IF NOT EXISTS tags_search TEXT[]; ALTER TABLE media_items ADD COLUMN IF NOT EXISTS contributors_search TEXT[]; +-- Audiobook editorial facts (A2a): narrators are user-editable first-class +-- (search/filter/badge surface); abridged rides along; audio_metadata keeps +-- the scan-derived tech record (duration/codec/chapters). +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; + -- Per-field user-override tracking (idempotent backfill for existing -- installs; the column is also declared in the media_items table -- definition above for fresh databases) @@ -268,6 +277,8 @@ CREATE INDEX IF NOT EXISTS idx_media_items_archived ON media_items (archived_at) -- Create GIN indexes for fast search field searches CREATE INDEX IF NOT EXISTS idx_media_items_tags_search ON media_items USING GIN (tags_search); CREATE INDEX IF NOT EXISTS idx_media_items_contributors_search ON media_items USING GIN (contributors_search); +CREATE INDEX IF NOT EXISTS idx_media_items_narrators ON media_items USING GIN (narrators); +CREATE INDEX IF NOT EXISTS idx_media_items_narrators_search ON media_items USING GIN (narrators_search); -- Drop existing triggers if exists. Both must be dropped BEFORE the function: -- dropping a function that triggers still reference fails (SQLSTATE 2BP01), diff --git a/internal/database/models.go b/internal/database/models.go index 64601125..9b6ac156 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -245,6 +245,8 @@ type MediaItems struct { 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"` @@ -315,6 +317,7 @@ type MediaItems struct { 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"` diff --git a/internal/database/querier.go b/internal/database/querier.go index 60f429fb..1c2587ea 100644 --- a/internal/database/querier.go +++ b/internal/database/querier.go @@ -503,6 +503,7 @@ type Querier interface { // Search Media Items queries SearchMediaItems(ctx context.Context, arg SearchMediaItemsParams) ([]SearchMediaItemsRow, error) SearchMediaItemsUnified(ctx context.Context, arg SearchMediaItemsUnifiedParams) ([]SearchMediaItemsUnifiedRow, error) + SearchNarratorValues(ctx context.Context, arg SearchNarratorValuesParams) ([]SearchNarratorValuesRow, error) SearchSeriesValues(ctx context.Context, arg SearchSeriesValuesParams) ([]SearchSeriesValuesRow, error) SearchTagsValues(ctx context.Context, arg SearchTagsValuesParams) ([]SearchTagsValuesRow, error) // Library Visibility queries diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index 8eb289a7..5e950029 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -979,10 +979,10 @@ 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) -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) +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) 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, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence +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 ` type CreateMediaItemParams struct { @@ -1031,6 +1031,9 @@ type CreateMediaItemParams struct { Summary pgtype.Text `db:"summary" json:"summary"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` + Narrators []string `db:"narrators" json:"narrators"` + NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` + Abridged bool `db:"abridged" json:"abridged"` } // Media Items queries @@ -1081,6 +1084,9 @@ func (q *Queries) CreateMediaItem(ctx context.Context, arg CreateMediaItemParams arg.Summary, arg.LibraryTypeName, arg.AudioMetadata, + arg.Narrators, + arg.NarratorsSearch, + arg.Abridged, ) var i MediaItems err := row.Scan( @@ -1097,6 +1103,8 @@ func (q *Queries) CreateMediaItem(ctx context.Context, arg CreateMediaItemParams &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -1145,6 +1153,7 @@ func (q *Queries) CreateMediaItem(ctx context.Context, arg CreateMediaItemParams &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -1203,10 +1212,10 @@ 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) -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) +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) 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, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence +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 ` type CreateMediaItemIfNewParams struct { @@ -1255,6 +1264,9 @@ type CreateMediaItemIfNewParams struct { Summary pgtype.Text `db:"summary" json:"summary"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` + Narrators []string `db:"narrators" json:"narrators"` + NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` + Abridged bool `db:"abridged" json:"abridged"` } // API create path: race-safe create-or-detect. On a duplicate @@ -1308,6 +1320,9 @@ func (q *Queries) CreateMediaItemIfNew(ctx context.Context, arg CreateMediaItemI arg.Summary, arg.LibraryTypeName, arg.AudioMetadata, + arg.Narrators, + arg.NarratorsSearch, + arg.Abridged, ) var i MediaItems err := row.Scan( @@ -1324,6 +1339,8 @@ func (q *Queries) CreateMediaItemIfNew(ctx context.Context, arg CreateMediaItemI &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -1372,6 +1389,7 @@ func (q *Queries) CreateMediaItemIfNew(ctx context.Context, arg CreateMediaItemI &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -2828,7 +2846,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, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items +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 tags @> ARRAY[$2::text] ORDER BY title ASC ` @@ -2861,6 +2879,8 @@ func (q *Queries) GetBooksByTag(ctx context.Context, arg GetBooksByTagParams) ([ &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -2909,6 +2929,7 @@ func (q *Queries) GetBooksByTag(ctx context.Context, arg GetBooksByTagParams) ([ &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -2952,7 +2973,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 +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 FROM collection_items ci JOIN media_items mi ON ci.media_item_id = mi.id WHERE ci.collection_id = $1 @@ -2972,6 +2993,7 @@ type GetCollectionItemsRow struct { Author pgtype.Text `db:"author" json:"author"` CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + FormatGroup string `db:"format_group" json:"format_group"` } // Get collection items @@ -2995,6 +3017,7 @@ func (q *Queries) GetCollectionItems(ctx context.Context, collectionID pgtype.UU &i.Author, &i.CoverImagePath, &i.LibraryID, + &i.FormatGroup, ); err != nil { return nil, err } @@ -3007,7 +3030,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.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.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, ci.excluded FROM media_items mi +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, ci.excluded 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) @@ -3037,6 +3060,8 @@ type GetCollectionItemsForDashboardRow struct { 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"` @@ -3085,6 +3110,7 @@ type GetCollectionItemsForDashboardRow struct { 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"` @@ -3115,6 +3141,8 @@ func (q *Queries) GetCollectionItemsForDashboard(ctx context.Context, arg GetCol &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -3163,6 +3191,7 @@ func (q *Queries) GetCollectionItemsForDashboard(ctx context.Context, arg GetCol &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -3316,7 +3345,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.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.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +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 INNER JOIN ( SELECT DISTINCT ON (media_item_id) media_item_id, last_read_at FROM reading_progress @@ -3362,6 +3391,8 @@ func (q *Queries) GetContinueReadingItems(ctx context.Context, arg GetContinueRe &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -3410,6 +3441,7 @@ func (q *Queries) GetContinueReadingItems(ctx context.Context, arg GetContinueRe &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -3439,7 +3471,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.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.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, + 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, usp.last_read_at FROM media_items mi JOIN user_series_progress usp ON mi.series = usp.series @@ -3449,7 +3481,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, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence, last_read_at FROM next_books +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 ORDER BY last_read_at DESC NULLS LAST LIMIT $1 ` @@ -3474,6 +3506,8 @@ type GetContinueSeriesItemsRow struct { 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"` @@ -3522,6 +3556,7 @@ type GetContinueSeriesItemsRow struct { 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"` @@ -3552,6 +3587,8 @@ func (q *Queries) GetContinueSeriesItems(ctx context.Context, arg GetContinueSer &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -3600,6 +3637,7 @@ func (q *Queries) GetContinueSeriesItems(ctx context.Context, arg GetContinueSer &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -4797,7 +4835,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.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.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +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 WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid) AND mi.archived_at IS NULL AND mi.missing_scan_count = 0 @@ -4827,6 +4865,8 @@ func (q *Queries) GetLibraryItems(ctx context.Context, libraryID pgtype.UUID) ([ &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -4875,6 +4915,7 @@ func (q *Queries) GetLibraryItems(ctx context.Context, libraryID pgtype.UUID) ([ &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -5302,7 +5343,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, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE id = $1 +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 ` func (q *Queries) GetMediaItem(ctx context.Context, id pgtype.UUID) (MediaItems, error) { @@ -5322,6 +5363,8 @@ func (q *Queries) GetMediaItem(ctx context.Context, id pgtype.UUID) (MediaItems, &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -5370,6 +5413,7 @@ func (q *Queries) GetMediaItem(ctx context.Context, id pgtype.UUID) (MediaItems, &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -5379,7 +5423,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, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_path = $1 AND library_id = $2 +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 ` type GetMediaItemByFilePathParams struct { @@ -5404,6 +5448,8 @@ func (q *Queries) GetMediaItemByFilePath(ctx context.Context, arg GetMediaItemBy &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -5452,6 +5498,7 @@ func (q *Queries) GetMediaItemByFilePath(ctx context.Context, arg GetMediaItemBy &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -5461,7 +5508,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, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_path = $1 LIMIT 1 +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 ` func (q *Queries) GetMediaItemByFilePathAnyLibrary(ctx context.Context, filePath string) (MediaItems, error) { @@ -5481,6 +5528,8 @@ func (q *Queries) GetMediaItemByFilePathAnyLibrary(ctx context.Context, filePath &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -5529,6 +5578,7 @@ func (q *Queries) GetMediaItemByFilePathAnyLibrary(ctx context.Context, filePath &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -5539,7 +5589,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, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_path = $1 +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 ` // ============================================ @@ -5562,6 +5612,8 @@ func (q *Queries) GetMediaItemByFilePathForSync(ctx context.Context, filePath st &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -5610,6 +5662,7 @@ func (q *Queries) GetMediaItemByFilePathForSync(ctx context.Context, filePath st &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -5619,7 +5672,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, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE kobo_content_id = $1 +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 ` func (q *Queries) GetMediaItemByKoboContentId(ctx context.Context, koboContentID pgtype.Text) (MediaItems, error) { @@ -5639,6 +5692,8 @@ func (q *Queries) GetMediaItemByKoboContentId(ctx context.Context, koboContentID &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -5687,6 +5742,7 @@ func (q *Queries) GetMediaItemByKoboContentId(ctx context.Context, koboContentID &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -5696,7 +5752,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, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE library_id = $1 AND file_path = $2 +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 ` type GetMediaItemByLibraryAndPathParams struct { @@ -5721,6 +5777,8 @@ func (q *Queries) GetMediaItemByLibraryAndPath(ctx context.Context, arg GetMedia &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -5769,6 +5827,7 @@ func (q *Queries) GetMediaItemByLibraryAndPath(ctx context.Context, arg GetMedia &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -5778,7 +5837,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, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE opf_identifier = $1 +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 ` // Get media item by OPF identifier @@ -5799,6 +5858,8 @@ func (q *Queries) GetMediaItemByOPFIdentifier(ctx context.Context, opfIdentifier &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -5847,6 +5908,7 @@ func (q *Queries) GetMediaItemByOPFIdentifier(ctx context.Context, opfIdentifier &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -5856,7 +5918,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, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE opf_uuid = $1 +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 ` // Get media item by OPF UUID @@ -5877,6 +5939,8 @@ func (q *Queries) GetMediaItemByOPFUUID(ctx context.Context, opfUuid pgtype.Text &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -5925,6 +5989,7 @@ func (q *Queries) GetMediaItemByOPFUUID(ctx context.Context, opfUuid pgtype.Text &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -5934,7 +5999,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, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_sha256 = $1 +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 ` // Get media item by SHA-256 hash @@ -5955,6 +6020,8 @@ func (q *Queries) GetMediaItemBySHA256(ctx context.Context, fileSha256 pgtype.Te &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -6003,6 +6070,7 @@ func (q *Queries) GetMediaItemBySHA256(ctx context.Context, fileSha256 pgtype.Te &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -6012,7 +6080,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, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_sha256 = $1 AND library_id = $2 +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 ` type GetMediaItemBySHA256AndLibraryParams struct { @@ -6038,6 +6106,8 @@ func (q *Queries) GetMediaItemBySHA256AndLibrary(ctx context.Context, arg GetMed &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -6086,6 +6156,7 @@ func (q *Queries) GetMediaItemBySHA256AndLibrary(ctx context.Context, arg GetMed &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -6210,7 +6281,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.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.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.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 FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id @@ -6242,6 +6313,8 @@ type GetMediaItemsBySHA256AndSameLibraryTypeRow struct { 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"` @@ -6290,6 +6363,7 @@ type GetMediaItemsBySHA256AndSameLibraryTypeRow struct { 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"` @@ -6325,6 +6399,8 @@ func (q *Queries) GetMediaItemsBySHA256AndSameLibraryType(ctx context.Context, a &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -6373,6 +6449,7 @@ func (q *Queries) GetMediaItemsBySHA256AndSameLibraryType(ctx context.Context, a &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -6625,7 +6702,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.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.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +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 WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid) AND mi.archived_at IS NULL AND mi.missing_scan_count = 0 @@ -6668,6 +6745,8 @@ func (q *Queries) GetNotStartedItems(ctx context.Context, arg GetNotStartedItems &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -6716,6 +6795,7 @@ func (q *Queries) GetNotStartedItems(ctx context.Context, arg GetNotStartedItems &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -7032,7 +7112,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.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.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +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 WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid) AND mi.archived_at IS NULL AND mi.missing_scan_count = 0 @@ -7068,6 +7148,8 @@ func (q *Queries) GetRecentlyAddedItems(ctx context.Context, arg GetRecentlyAdde &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -7116,6 +7198,7 @@ func (q *Queries) GetRecentlyAddedItems(ctx context.Context, arg GetRecentlyAdde &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -7132,7 +7215,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.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.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +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 INNER JOIN ( SELECT DISTINCT ON (media_item_id) media_item_id, last_read_at FROM reading_progress @@ -7176,6 +7259,8 @@ func (q *Queries) GetRecentlyReadItems(ctx context.Context, arg GetRecentlyReadI &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -7224,6 +7309,7 @@ func (q *Queries) GetRecentlyReadItems(ctx context.Context, arg GetRecentlyReadI &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -7340,7 +7426,7 @@ 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, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items +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 series = $1 AND library_id = $2 AND archived_at IS NULL @@ -7379,6 +7465,8 @@ func (q *Queries) GetSeriesBooks(ctx context.Context, arg GetSeriesBooksParams) &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -7427,6 +7515,7 @@ func (q *Queries) GetSeriesBooks(ctx context.Context, arg GetSeriesBooksParams) &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -9073,7 +9162,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.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.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +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 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 @@ -9114,6 +9203,8 @@ func (q *Queries) GetVisibleLiveMediaForRules(ctx context.Context, arg GetVisibl &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -9162,6 +9253,7 @@ func (q *Queries) GetVisibleLiveMediaForRules(ctx context.Context, arg GetVisibl &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -9680,7 +9772,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.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.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.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, twin.id AS match_id, COALESCE(twin.title, '') AS match_title, twin_library.name AS match_library_name FROM media_items mi @@ -9720,6 +9812,8 @@ type ListHiddenMediaItemsRow struct { 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"` @@ -9768,6 +9862,7 @@ type ListHiddenMediaItemsRow struct { 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"` @@ -9808,6 +9903,8 @@ func (q *Queries) ListHiddenMediaItems(ctx context.Context) ([]ListHiddenMediaIt &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -9856,6 +9953,7 @@ func (q *Queries) ListHiddenMediaItems(ctx context.Context) ([]ListHiddenMediaIt &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -9926,7 +10024,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.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.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.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 FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id @@ -9956,6 +10054,8 @@ type ListMediaItemsRow struct { 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"` @@ -10004,6 +10104,7 @@ type ListMediaItemsRow struct { 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"` @@ -10035,6 +10136,8 @@ func (q *Queries) ListMediaItems(ctx context.Context, arg ListMediaItemsParams) &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -10083,6 +10186,7 @@ func (q *Queries) ListMediaItems(ctx context.Context, arg ListMediaItemsParams) &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -10101,7 +10205,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.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.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.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 FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id @@ -10125,6 +10229,8 @@ type ListMediaItemsByLibraryRow struct { 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"` @@ -10173,6 +10279,7 @@ type ListMediaItemsByLibraryRow struct { 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"` @@ -10204,6 +10311,8 @@ func (q *Queries) ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype. &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -10252,6 +10361,7 @@ func (q *Queries) ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype. &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -10270,7 +10380,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.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.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.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 FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id @@ -10292,6 +10402,8 @@ type ListMediaItemsByLibraryIncludingArchivedRow struct { 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"` @@ -10340,6 +10452,7 @@ type ListMediaItemsByLibraryIncludingArchivedRow struct { 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"` @@ -10371,6 +10484,8 @@ func (q *Queries) ListMediaItemsByLibraryIncludingArchived(ctx context.Context, &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -10419,6 +10534,7 @@ func (q *Queries) ListMediaItemsByLibraryIncludingArchived(ctx context.Context, &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -10437,7 +10553,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, 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, 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, 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 ` type ListMediaItemsBySHA256AndLibraryParams struct { @@ -10469,6 +10585,8 @@ func (q *Queries) ListMediaItemsBySHA256AndLibrary(ctx context.Context, arg List &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -10517,6 +10635,7 @@ func (q *Queries) ListMediaItemsBySHA256AndLibrary(ctx context.Context, arg List &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -10533,7 +10652,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, 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, 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, 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 ` // List media items that have no stored SHA-256 (imported before hashing existed) @@ -10560,6 +10679,8 @@ func (q *Queries) ListMediaItemsMissingHash(ctx context.Context) ([]MediaItems, &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -10608,6 +10729,7 @@ func (q *Queries) ListMediaItemsMissingHash(ctx context.Context) ([]MediaItems, &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -10624,7 +10746,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.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.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.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 FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id @@ -10721,6 +10843,8 @@ type ListMediaItemsSortedRow struct { 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"` @@ -10769,6 +10893,7 @@ type ListMediaItemsSortedRow struct { 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"` @@ -10805,6 +10930,8 @@ func (q *Queries) ListMediaItemsSorted(ctx context.Context, arg ListMediaItemsSo &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -10853,6 +10980,7 @@ func (q *Queries) ListMediaItemsSorted(ctx context.Context, arg ListMediaItemsSo &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -12159,7 +12287,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.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.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.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 FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id @@ -12209,6 +12337,8 @@ type SearchMediaItemsRow struct { 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"` @@ -12257,6 +12387,7 @@ type SearchMediaItemsRow struct { 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"` @@ -12295,6 +12426,8 @@ func (q *Queries) SearchMediaItems(ctx context.Context, arg SearchMediaItemsPara &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -12343,6 +12476,7 @@ func (q *Queries) SearchMediaItems(ctx context.Context, arg SearchMediaItemsPara &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -12402,6 +12536,7 @@ SELECT mi.kobo_metadata, mi.tags_search, mi.contributors_search, + mi.narrators, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, @@ -12432,39 +12567,49 @@ WHERE COALESCE(lv.is_visible, true) = true SELECT 1 FROM unnest(mi.tags_search) AS tag WHERE word_similarity($8, tag) > 0.3 )) + -- Narrator filter (A2a โ arrays use the *_search column) + AND ($9 = '' OR EXISTS ( + SELECT 1 FROM unnest(mi.narrators_search) AS narrator + WHERE word_similarity($9, narrator) > 0.3 + )) -- Year range (exact) - prioritize date_published, fallback to copyright_year AND ( - $9 = 0 OR - EXTRACT(YEAR FROM mi.date_published) >= $9 OR - (mi.date_published IS NULL AND mi.copyright_year >= $9) + $10 = 0 OR + EXTRACT(YEAR FROM mi.date_published) >= $10 OR + (mi.date_published IS NULL AND mi.copyright_year >= $10) ) AND ( - $10 = 0 OR - EXTRACT(YEAR FROM mi.date_published) <= $10 OR - (mi.date_published IS NULL AND mi.copyright_year <= $10) + $11 = 0 OR + EXTRACT(YEAR FROM mi.date_published) <= $11 OR + (mi.date_published IS NULL AND mi.copyright_year <= $11) ) -- Boolean (exact) AND ( - $11::bool IS NULL OR -- Not specified = show all - ($11::bool IS TRUE AND mi.cover_image_path IS NOT NULL) OR - ($11::bool IS FALSE AND mi.cover_image_path IS NULL) + $12::bool IS NULL OR -- Not specified = show all + ($12::bool IS TRUE AND mi.cover_image_path IS NOT NULL) OR + ($12::bool IS FALSE AND mi.cover_image_path IS NULL) ) -- Search query (fuzzy or exact based on quotes) AND ( - $12 = '' OR + $13 = '' OR -- Fuzzy search (default) - $13 = false AND ( - word_similarity($12, mi.title) > 0.3 OR - word_similarity($12, COALESCE(mi.author, '')) > 0.3 OR - word_similarity($12, COALESCE(mi.series, '')) > 0.3 OR + $14 = false AND ( + word_similarity($13, mi.title) > 0.3 OR + word_similarity($13, COALESCE(mi.author, '')) > 0.3 OR + word_similarity($13, COALESCE(mi.series, '')) > 0.3 OR EXISTS ( SELECT 1 FROM unnest(mi.tags_search) AS tag - WHERE word_similarity($12, tag) > 0.3 + WHERE word_similarity($13, tag) > 0.3 LIMIT 1 ) OR EXISTS ( SELECT 1 FROM unnest(mi.contributors_search) AS contributor - WHERE word_similarity($12, contributor) > 0.3 + WHERE word_similarity($13, contributor) > 0.3 + LIMIT 1 + ) OR + EXISTS ( + SELECT 1 FROM unnest(mi.narrators_search) AS narrator + WHERE word_similarity($13, narrator) > 0.3 LIMIT 1 ) ) OR @@ -12478,24 +12623,25 @@ WHERE COALESCE(lv.is_visible, true) = true -- sqlc.narg('search_pattern') = ANY(mi.contributors_search) -- ) -- Exact search (with quotes) - true exact match, not substring - $13 = true AND ( - mi.title = $12 OR - COALESCE(mi.author, '') = $12 OR - COALESCE(mi.series, '') = $12 OR - $12 = ANY(mi.tags_search) OR - $12 = ANY(mi.contributors_search) + $14 = true AND ( + mi.title = $13 OR + COALESCE(mi.author, '') = $13 OR + COALESCE(mi.series, '') = $13 OR + $13 = ANY(mi.tags_search) OR + $13 = ANY(mi.contributors_search) OR + $13 = ANY(mi.narrators_search) ) ) ORDER BY -- Primary sort: relevance score when searching CASE - WHEN $12 != '' THEN + WHEN $13 != '' THEN GREATEST( - CASE WHEN $13 = false THEN - word_similarity($12, mi.title) + CASE WHEN $14 = false THEN + word_similarity($13, mi.title) ELSE 0 END, - CASE WHEN $13 = false THEN - word_similarity($12, COALESCE(mi.author, '')) + CASE WHEN $14 = false THEN + word_similarity($13, COALESCE(mi.author, '')) ELSE 0 END, word_similarity($3, COALESCE(mi.author, '')), word_similarity($5, COALESCE(mi.genre, '')), @@ -12507,40 +12653,40 @@ ORDER BY END DESC, -- Secondary sort: user-specified sort parameter CASE - WHEN $14 = 'title ASC' THEN mi.title + WHEN $15 = 'title ASC' THEN mi.title ELSE '' END ASC, CASE - WHEN $14 = 'title DESC' THEN mi.title + WHEN $15 = 'title DESC' THEN mi.title ELSE '' END DESC, CASE - WHEN $14 = 'author ASC' THEN COALESCE(mi.author, '') + WHEN $15 = 'author ASC' THEN COALESCE(mi.author, '') ELSE '' END ASC, CASE - WHEN $14 = 'author DESC' THEN COALESCE(mi.author, '') + WHEN $15 = 'author DESC' THEN COALESCE(mi.author, '') ELSE '' END DESC, CASE - WHEN $14 = 'created_at ASC' THEN mi.created_at + WHEN $15 = 'created_at ASC' THEN mi.created_at ELSE '1970-01-01'::timestamp END ASC, CASE - WHEN $14 = 'created_at DESC' THEN mi.created_at + WHEN $15 = 'created_at DESC' THEN mi.created_at ELSE '1970-01-01'::timestamp END DESC, CASE - WHEN $14 = 'page_count ASC' THEN COALESCE(mi.page_count::text, '0') + WHEN $15 = 'page_count ASC' THEN COALESCE(mi.page_count::text, '0') ELSE '' END ASC, CASE - WHEN $14 = 'page_count DESC' THEN COALESCE(mi.page_count::text, '0') + WHEN $15 = 'page_count DESC' THEN COALESCE(mi.page_count::text, '0') ELSE '' END DESC, -- Tertiary sort: title (default fallback) mi.title ASC -LIMIT $16 OFFSET $15 +LIMIT $17 OFFSET $16 ` type SearchMediaItemsUnifiedParams struct { @@ -12552,6 +12698,7 @@ type SearchMediaItemsUnifiedParams struct { PublisherFilter interface{} `db:"publisher_filter" json:"publisher_filter"` LanguageFilter interface{} `db:"language_filter" json:"language_filter"` TagsFilter interface{} `db:"tags_filter" json:"tags_filter"` + NarratorFilter interface{} `db:"narrator_filter" json:"narrator_filter"` YearMin interface{} `db:"year_min" json:"year_min"` YearMax interface{} `db:"year_max" json:"year_max"` HasCover pgtype.Bool `db:"has_cover" json:"has_cover"` @@ -12603,6 +12750,7 @@ type SearchMediaItemsUnifiedRow struct { KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"` TagsSearch []string `db:"tags_search" json:"tags_search"` ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` + Narrators []string `db:"narrators" json:"narrators"` 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"` @@ -12621,6 +12769,7 @@ func (q *Queries) SearchMediaItemsUnified(ctx context.Context, arg SearchMediaIt arg.PublisherFilter, arg.LanguageFilter, arg.TagsFilter, + arg.NarratorFilter, arg.YearMin, arg.YearMax, arg.HasCover, @@ -12678,6 +12827,7 @@ func (q *Queries) SearchMediaItemsUnified(ctx context.Context, arg SearchMediaIt &i.KoboMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.Narrators, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -12695,6 +12845,64 @@ func (q *Queries) SearchMediaItemsUnified(ctx context.Context, arg SearchMediaIt return items, nil } +const SearchNarratorValues = `-- name: SearchNarratorValues :many +SELECT + narrator::TEXT as value, + COUNT(*) as count, + word_similarity($1, narrator)::float8 as score +FROM media_items mi +JOIN libraries l ON mi.library_id = l.id +LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2 +CROSS JOIN LATERAL unnest(mi.narrators) AS narrator +WHERE COALESCE(lv.is_visible, true) = true + AND ($3::uuid IS NULL OR mi.library_id = $3::uuid) + AND narrator IS NOT NULL + AND word_similarity($1, narrator) > 0.3 +GROUP BY narrator, word_similarity($1, narrator)::float8 +ORDER BY word_similarity($1, narrator)::float8 DESC, COUNT DESC +LIMIT $5 OFFSET $4 +` + +type SearchNarratorValuesParams struct { + SearchQuery pgtype.Text `db:"search_query" json:"search_query"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + Offset pgtype.Int4 `db:"offset" json:"offset"` + Limit pgtype.Int4 `db:"limit" json:"limit"` +} + +type SearchNarratorValuesRow struct { + Value string `db:"value" json:"value"` + Count int64 `db:"count" json:"count"` + Score float64 `db:"score" json:"score"` +} + +func (q *Queries) SearchNarratorValues(ctx context.Context, arg SearchNarratorValuesParams) ([]SearchNarratorValuesRow, error) { + rows, err := q.db.Query(ctx, SearchNarratorValues, + arg.SearchQuery, + arg.UserID, + arg.LibraryID, + arg.Offset, + arg.Limit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + items := []SearchNarratorValuesRow{} + for rows.Next() { + var i SearchNarratorValuesRow + if err := rows.Scan(&i.Value, &i.Count, &i.Score); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const SearchSeriesValues = `-- name: SearchSeriesValues :many SELECT mi.series as value, @@ -13885,9 +14093,12 @@ UPDATE media_items SET summary = $37, metadata_overrides = $38, audio_metadata = $39, + narrators = $40, + narrators_search = $41, + abridged = $42, 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, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence +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 ` type UpdateMediaItemParams struct { @@ -13930,6 +14141,9 @@ type UpdateMediaItemParams struct { Summary pgtype.Text `db:"summary" json:"summary"` MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` + Narrators []string `db:"narrators" json:"narrators"` + NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` + Abridged bool `db:"abridged" json:"abridged"` } func (q *Queries) UpdateMediaItem(ctx context.Context, arg UpdateMediaItemParams) (MediaItems, error) { @@ -13973,6 +14187,9 @@ func (q *Queries) UpdateMediaItem(ctx context.Context, arg UpdateMediaItemParams arg.Summary, arg.MetadataOverrides, arg.AudioMetadata, + arg.Narrators, + arg.NarratorsSearch, + arg.Abridged, ) var i MediaItems err := row.Scan( @@ -13989,6 +14206,8 @@ func (q *Queries) UpdateMediaItem(ctx context.Context, arg UpdateMediaItemParams &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -14037,6 +14256,7 @@ func (q *Queries) UpdateMediaItem(ctx context.Context, arg UpdateMediaItemParams &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -14049,7 +14269,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, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence +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 ` type UpdateMediaItemChapterMetadataParams struct { @@ -14074,6 +14294,8 @@ func (q *Queries) UpdateMediaItemChapterMetadata(ctx context.Context, arg Update &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -14122,6 +14344,7 @@ func (q *Queries) UpdateMediaItemChapterMetadata(ctx context.Context, arg Update &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -14225,7 +14448,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, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence +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 ` type UpdateMediaItemIdentifiersParams struct { @@ -14264,6 +14487,8 @@ func (q *Queries) UpdateMediaItemIdentifiers(ctx context.Context, arg UpdateMedi &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -14312,6 +14537,7 @@ func (q *Queries) UpdateMediaItemIdentifiers(ctx context.Context, arg UpdateMedi &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, @@ -14328,7 +14554,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, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence +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 ` type UpdateMediaItemKoboMetadataParams struct { @@ -14362,6 +14588,8 @@ func (q *Queries) UpdateMediaItemKoboMetadata(ctx context.Context, arg UpdateMed &i.Series, &i.SeriesNumber, &i.Tags, + &i.Narrators, + &i.Abridged, &i.Asin, &i.DatePublished, &i.Publisher, @@ -14410,6 +14638,7 @@ func (q *Queries) UpdateMediaItemKoboMetadata(ctx context.Context, arg UpdateMed &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, + &i.NarratorsSearch, &i.FileSha256, &i.OpfIdentifier, &i.OpfUuid, diff --git a/internal/database/queries/queries.sql b/internal/database/queries/queries.sql index 9b301a31..a32b8cfa 100644 --- a/internal/database/queries/queries.sql +++ b/internal/database/queries/queries.sql @@ -149,8 +149,8 @@ GROUP BY l.id; -- Media Items queries -- 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) -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) +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) ON CONFLICT (library_id, file_path) DO UPDATE SET updated_at = NOW() RETURNING *; @@ -159,8 +159,8 @@ RETURNING *; -- (library_id, file_path) this inserts NOTHING and returns no row -- (pgx.ErrNoRows) โ the handler then answers 200 with the existing row -- and a duplicate flag instead of pretending it created something. -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) -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) +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) ON CONFLICT (library_id, file_path) DO NOTHING RETURNING *; @@ -314,6 +314,9 @@ UPDATE media_items SET summary = $37, metadata_overrides = $38, audio_metadata = $39, + narrators = $40, + narrators_search = $41, + abridged = $42, updated_at = NOW() WHERE id = $1 RETURNING *; @@ -649,6 +652,7 @@ SELECT mi.kobo_metadata, mi.tags_search, mi.contributors_search, + mi.narrators, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, @@ -679,6 +683,11 @@ WHERE COALESCE(lv.is_visible, true) = true SELECT 1 FROM unnest(mi.tags_search) AS tag WHERE word_similarity(sqlc.narg('tags_filter'), tag) > 0.3 )) + -- Narrator filter (A2a โ arrays use the *_search column) + AND (sqlc.narg('narrator_filter') = '' OR EXISTS ( + SELECT 1 FROM unnest(mi.narrators_search) AS narrator + WHERE word_similarity(sqlc.narg('narrator_filter'), narrator) > 0.3 + )) -- Year range (exact) - prioritize date_published, fallback to copyright_year AND ( sqlc.narg('year_min') = 0 OR @@ -713,6 +722,11 @@ WHERE COALESCE(lv.is_visible, true) = true SELECT 1 FROM unnest(mi.contributors_search) AS contributor WHERE word_similarity(sqlc.narg('search_query'), contributor) > 0.3 LIMIT 1 + ) OR + EXISTS ( + SELECT 1 FROM unnest(mi.narrators_search) AS narrator + WHERE word_similarity(sqlc.narg('search_query'), narrator) > 0.3 + LIMIT 1 ) ) OR -- Exact search (with quotes) @@ -730,7 +744,8 @@ WHERE COALESCE(lv.is_visible, true) = true COALESCE(mi.author, '') = sqlc.narg('search_query') OR COALESCE(mi.series, '') = sqlc.narg('search_query') OR sqlc.narg('search_query') = ANY(mi.tags_search) OR - sqlc.narg('search_query') = ANY(mi.contributors_search) + sqlc.narg('search_query') = ANY(mi.contributors_search) OR + sqlc.narg('search_query') = ANY(mi.narrators_search) ) ) ORDER BY @@ -844,6 +859,23 @@ GROUP BY tag, word_similarity(sqlc.narg('search_query'), tag)::float8 ORDER BY word_similarity(sqlc.narg('search_query'), tag)::float8 DESC, COUNT DESC LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); +-- name: SearchNarratorValues :many +SELECT + narrator::TEXT as value, + COUNT(*) as count, + word_similarity(sqlc.narg('search_query'), narrator)::float8 as score +FROM media_items mi +JOIN libraries l ON mi.library_id = l.id +LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id') +CROSS JOIN LATERAL unnest(mi.narrators) AS narrator +WHERE COALESCE(lv.is_visible, true) = true + AND (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) + AND narrator IS NOT NULL + AND word_similarity(sqlc.narg('search_query'), narrator) > 0.3 +GROUP BY narrator, word_similarity(sqlc.narg('search_query'), narrator)::float8 +ORDER BY word_similarity(sqlc.narg('search_query'), narrator)::float8 DESC, COUNT DESC +LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); + -- name: SearchSeriesValues :many SELECT mi.series as value, @@ -2367,7 +2399,7 @@ DELETE FROM collection_items WHERE collection_id = $1 AND media_item_id = $2; -- Get collection items -- name: GetCollectionItems :many -SELECT ci.*, mi.title, mi.author, mi.cover_image_path, mi.library_id +SELECT ci.*, 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 diff --git a/internal/handlers/collections.go b/internal/handlers/collections.go index 2c776b26..dee8431f 100644 --- a/internal/handlers/collections.go +++ b/internal/handlers/collections.go @@ -75,6 +75,8 @@ type BookInfo struct { Author string `json:"author"` CoverImagePath string `json:"cover_image_path"` HasConflict bool `json:"has_conflict"` + // Media-type identifier for card badges/hover icons (A2c) + FormatGroup string `json:"format_group"` } type SectionData struct { @@ -283,6 +285,7 @@ func (h *CollectionHandler) GetCollection(c *echo.Context) error { Title: item.Title, Author: textToString(item.Author), CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath), + FormatGroup: item.FormatGroup, }) } } else if collection.IsSystemCollection.Bool && collection.QueryType.Valid && collection.QueryType.String != "" && collection.QueryType.String != "filter" { @@ -333,6 +336,7 @@ func (h *CollectionHandler) GetCollection(c *echo.Context) error { Title: item.Title, Author: textToString(item.Author), CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath), + FormatGroup: item.FormatGroup, } } bookList = bookCards @@ -349,6 +353,7 @@ func (h *CollectionHandler) GetCollection(c *echo.Context) error { Title: book.Title, Author: textToString(book.Author), CoverImagePath: utils.ResolveMediaURL(book.LibraryID, book.CoverImagePath), + FormatGroup: book.FormatGroup, }) } } diff --git a/internal/handlers/conflicts.go b/internal/handlers/conflicts.go index 74de54b7..7ba495c9 100644 --- a/internal/handlers/conflicts.go +++ b/internal/handlers/conflicts.go @@ -392,18 +392,18 @@ func (h *ConflictHandler) applyHighlightResolution(ctx context.Context, mediaIte } params := database.UpdateMediaHighlightForSyncParams{ - ID: existing.ID, - SelectionText: existing.SelectionText, - StartPosition: existing.StartPosition, - EndPosition: existing.EndPosition, - Color: existing.Color, - NoteText: existing.NoteText, - PercentageStart: existing.PercentageStart, - PercentageEnd: existing.PercentageEnd, - EpubcfiStart: existing.EpubcfiStart, - EpubcfiEnd: existing.EpubcfiEnd, - ChapterReference: existing.ChapterReference, - LastModifiedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true}, + ID: existing.ID, + SelectionText: existing.SelectionText, + StartPosition: existing.StartPosition, + EndPosition: existing.EndPosition, + Color: existing.Color, + NoteText: existing.NoteText, + PercentageStart: existing.PercentageStart, + PercentageEnd: existing.PercentageEnd, + EpubcfiStart: existing.EpubcfiStart, + EpubcfiEnd: existing.EpubcfiEnd, + ChapterReference: existing.ChapterReference, + LastModifiedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true}, LastModifiedSource: pgtype.Text{String: "conflict_resolution", Valid: true}, DeviceSyncData: existing.DeviceSyncData, UserID: userID, @@ -439,17 +439,17 @@ func (h *ConflictHandler) applyBookmarkResolution(ctx context.Context, mediaItem } params := database.UpdateMediaBookmarkForSyncParams{ - ID: existing.ID, - PageNumber: existing.PageNumber, - ChapterNumber: existing.ChapterNumber, - CfiPosition: existing.CfiPosition, - Title: existing.Title, - Position: existing.Position, - Notes: existing.Notes, + ID: existing.ID, + PageNumber: existing.PageNumber, + ChapterNumber: existing.ChapterNumber, + CfiPosition: existing.CfiPosition, + Title: existing.Title, + Position: existing.Position, + Notes: existing.Notes, PercentageLocation: existing.PercentageLocation, - EpubcfiLocation: existing.EpubcfiLocation, - ChapterReference: existing.ChapterReference, - LastModifiedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true}, + EpubcfiLocation: existing.EpubcfiLocation, + ChapterReference: existing.ChapterReference, + LastModifiedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true}, LastModifiedSource: pgtype.Text{String: "conflict_resolution", Valid: true}, DeviceSyncData: existing.DeviceSyncData, UserID: userID, diff --git a/internal/handlers/dashboard.go b/internal/handlers/dashboard.go index 00242ad0..f361cb2f 100644 --- a/internal/handlers/dashboard.go +++ b/internal/handlers/dashboard.go @@ -64,7 +64,17 @@ func (h *DashboardHandler) GetSections(c *echo.Context) error { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to load dashboard sections"}) } - sectionData := BuildSections(sections, libraryID) + // Per-type wording: system-collection display swaps on audiobook scope. + libraryType := "" + if libraryID != "" { + if pgLibID, perr := uuid.Parse(libraryID); perr == nil { + if lib, lerr := h.db.GetLibrary(c.Request().Context(), pgtype.UUID{Bytes: pgLibID, Valid: true}); lerr == nil { + libraryType = lib.TypeName + } + } + } + + sectionData := BuildSections(sections, libraryID, libraryType) sectionData = MarkActiveConflictsSections(c.Request().Context(), h.db, user.ID, sectionData) return c.JSON(http.StatusOK, map[string]interface{}{"sections": sectionData}) @@ -168,10 +178,27 @@ func (h *DashboardHandler) RestoreSystemCollection(c *echo.Context) error { return c.JSON(http.StatusOK, map[string]string{"message": "System collection restored to defaults"}) } -func BuildSections(sections []services.DashboardSection, currentLibraryID string) []SectionData { +// BuildSections maps dashboard service sections to card data. When the +// active library is audiobooks, system-collection titles/descriptions +// swap to the listening wording at display time โ the stored names stay +// generic so restores and settings keep working across library types. +func BuildSections(sections []services.DashboardSection, currentLibraryID, libraryType string) []SectionData { var result []SectionData + audioScope := libraryType == "audiobooks" + for _, ds := range sections { + if audioScope && ds.IsSystem { + switch ds.Title { + case "Continue Reading": + ds.Title = "Continue Listening" + ds.Description = "Audiobooks you're currently listening to (0 < progress < 1)" + case "Recently Read": + ds.Title = "Recently Listened" + ds.Description = "Audiobooks you've finished (progress >= 1)" + } + } + bookCards := make([]BookInfo, len(ds.Items)) for i, item := range ds.Items { itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16]) @@ -180,6 +207,7 @@ func BuildSections(sections []services.DashboardSection, currentLibraryID string Title: item.Title, Author: textToString(item.Author), CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath), + FormatGroup: item.FormatGroup, } } diff --git a/internal/handlers/media.go b/internal/handlers/media.go index fedf2540..ca5cd9a8 100644 --- a/internal/handlers/media.go +++ b/internal/handlers/media.go @@ -59,6 +59,9 @@ type CreateMediaItemRequest struct { AlternateInfo string `json:"alternate_info"` // JSON string ScanInformation string `json:"scan_information"` Summary string `json:"summary"` + // Audio editorial facts (A2a) + Narrators []string `json:"narrators"` + Abridged bool `json:"abridged"` } // UpdateMediaItemRequest represents the request for updating a media item @@ -98,6 +101,10 @@ type UpdateMediaItemRequest struct { AlternateInfo string `form:"alternate_info" json:"alternate_info"` ScanInformation string `form:"scan_information" json:"scan_information"` Summary string `form:"summary" json:"summary"` + // Audiobook editorial facts (A2a): narrators verbatim display order; + // abridged as the Libation/Audible flag. + Narrators []string `form:"narrators" json:"narrators"` + Abridged bool `form:"abridged" json:"abridged"` } // CreateMediaNoteRequest represents the request for creating a media note @@ -897,6 +904,8 @@ func (mh *MediaHandler) GetMediaItem(c *echo.Context) error { "updated_at": item.UpdatedAt.Time.Format("2006-01-02T15:04:05Z07:00"), "format_group": item.FormatGroup, "format_mimetype": textToString(item.FormatMimetype), + "narrators": item.Narrators, + "abridged": item.Abridged, // NEW: Reading direction fields "manga_type": textToString(item.MangaType), "reading_direction": textToString(item.ReadingDirection), @@ -1405,6 +1414,7 @@ func (mh *MediaHandler) CreateMediaItem(c *echo.Context) error { tagsSearch := utils.NormalizeTagsSearch(req.Tags) contributorsSearch := utils.NormalizeContributorsSearch(req.Contributors) + narratorsSearch := utils.NormalizeNarratorsSearch(req.Narrators) // Validate and normalize ISBN normalizedISBN, err := utils.NormalizeISBN(req.ISBN) @@ -1454,6 +1464,9 @@ func (mh *MediaHandler) CreateMediaItem(c *echo.Context) error { SeriesNumber: pgtype.Int4{Int32: req.SeriesNumber, Valid: req.SeriesNumber > 0}, Tags: req.Tags, TagsSearch: tagsSearch, + Narrators: req.Narrators, + NarratorsSearch: narratorsSearch, + Abridged: req.Abridged, 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 != ""}, @@ -1519,6 +1532,7 @@ func (mh *MediaHandler) UpdateMediaItem(c *echo.Context) error { tagsSearch := utils.NormalizeTagsSearch(req.Tags) contributorsSearch := utils.NormalizeContributorsSearch(req.Contributors) + narratorsSearch := utils.NormalizeNarratorsSearch(req.Narrators) normalizedISBN, err := utils.NormalizeISBN(req.ISBN) if err != nil && req.ISBN != "" { @@ -1572,6 +1586,9 @@ func (mh *MediaHandler) UpdateMediaItem(c *echo.Context) error { SeriesNumber: pgtype.Int4{Int32: req.SeriesNumber, Valid: req.SeriesNumber > 0}, Tags: req.Tags, TagsSearch: tagsSearch, + Narrators: req.Narrators, + NarratorsSearch: narratorsSearch, + Abridged: req.Abridged, 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 != ""}, @@ -2427,6 +2444,9 @@ func (mh *MediaHandler) SearchMediaItems(c *echo.Context) error { if tags := c.QueryParam("tags"); tags != "" { return mh.handleFieldValuesSearch(c, userID.ID, "tags", tags) } + if narrator := c.QueryParam("narrator"); narrator != "" { + return mh.handleFieldValuesSearch(c, userID.ID, "narrator", narrator) + } if series := c.QueryParam("series"); series != "" { return mh.handleFieldValuesSearch(c, userID.ID, "series", series) } @@ -2468,6 +2488,7 @@ func (mh *MediaHandler) SearchMediaItems(c *echo.Context) error { genreFilter := c.QueryParam("genre_filter") publisherFilter := c.QueryParam("publisher_filter") tagsFilter := c.QueryParam("tags_filter") + narratorFilter := c.QueryParam("narrator_filter") languageFilter := c.QueryParam("language_filter") yearMin, _ := strconv.Atoi(c.QueryParam("year_min")) yearMax, _ := strconv.Atoi(c.QueryParam("year_max")) @@ -2494,6 +2515,7 @@ func (mh *MediaHandler) SearchMediaItems(c *echo.Context) error { GenreFilter: genreFilter, PublisherFilter: publisherFilter, TagsFilter: tagsFilter, + NarratorFilter: narratorFilter, LanguageFilter: languageFilter, YearMin: yearMin, YearMax: yearMax, diff --git a/internal/handlers/media_detail.go b/internal/handlers/media_detail.go index 5c948570..0378bc12 100644 --- a/internal/handlers/media_detail.go +++ b/internal/handlers/media_detail.go @@ -1,12 +1,22 @@ package handlers -import "bookhoard/internal/database" +import ( + "encoding/json" + "fmt" + + "bookhoard/internal/database" +) // MediaDetail embeds database.MediaItems for complete book metadata // No field duplication - template gets direct access to all database fields type MediaDetail struct { database.MediaItems // Embedded - ALL book fields available + // Audio editorial/tech surfaces (A2a): duration parsed out of the + // audio_metadata JSONB the scanner wrote (display-only). + audioDurationSeconds float64 + audioParsed bool + // Absolute on-disk location (library folder + relative path), resolved by // the page handler so the UI can show where the file lives. FileLocation string `json:"file_location"` @@ -37,6 +47,7 @@ type MediaDetail struct { // BookStatsData is the per-book reading-stats card payload. type BookStatsData struct { TotalMinutes int + TotalSeconds int // exact wall-clock; audio "Time listened" displays from this TotalPages int SessionCount int MaxProgress float64 // 0..1 @@ -44,3 +55,140 @@ type BookStatsData struct { LastSession string FirstSession string } + +// IsAudio reports whether the item's format group is the dedicated +// audiobook group (drives every "reading vs listening" wording swap). +func (md *MediaDetail) IsAudio() bool { + return md.FormatGroup == "audiobook" +} + +// --- audio-aware copy helpers (A2c): one home per verb so web + future +// clients agree; format_group chooses the string --- + +func (md *MediaDetail) ActionLabel() string { + if md.IsAudio() { + return "Listen Now" + } + return "Read Now" +} + +func (md *MediaDetail) ActionIcon() string { + if md.IsAudio() { + return "headphones" + } + return "book-open" +} + +func (md *MediaDetail) MarkCompleteLabel() string { + if md.IsAudio() { + return "Mark Complete" + } + return "Mark Read" +} + +func (md *MediaDetail) MarkResetLabel() string { + if md.IsAudio() { + return "Mark Not Complete" + } + return "Mark Unread" +} + +func (md *MediaDetail) ProgressHeader() string { + if md.IsAudio() { + return "Listening Progress" + } + return "Reading Progress" +} + +func (md *MediaDetail) LastReadLabel() string { + if md.IsAudio() { + return "Last Listen" + } + return "Last Read" +} + +func (md *MediaDetail) StatsHeader() string { + if md.IsAudio() { + return "Your Listening Stats" + } + return "Your Reading Stats" +} + +func (md *MediaDetail) TimeStatLabel() string { + if md.IsAudio() { + return "Time Listening" + } + return "Time Reading" +} + +// AudioDuration lazily parses duration_seconds out of audio_metadata. +func (md *MediaDetail) AudioDuration() float64 { + if !md.audioParsed { + md.audioParsed = true + var payload struct { + DurationSeconds float64 `json:"duration_seconds"` + } + if len(md.AudioMetadata) > 0 && json.Unmarshal(md.AudioMetadata, &payload) == nil { + md.audioDurationSeconds = payload.DurationSeconds + } + } + return md.audioDurationSeconds +} + +// AudioDurationLabel renders H:MM:SS (or M:SS under an hour) for the +// audio surfaces โ same shape the web player shows. +func (md *MediaDetail) AudioDurationLabel() string { + s := int(md.AudioDuration() + 0.5) + if s <= 0 { + return "โ" + } + h := s / 3600 + m := (s % 3600) / 60 + sec := s % 60 + if h > 0 { + return fmt.Sprintf("%d:%02d:%02d", h, m, sec) + } + return fmt.Sprintf("%d:%02d", m, sec) +} + +// AudioCurrentSeconds is the playhead in seconds: percentage of the +// whole-book duration (chapter-exact granularity lives in the player; +// the detail page shows shelf-level position). +func (md *MediaDetail) AudioCurrentSeconds() int { + dur := md.AudioDuration() + if dur <= 0 || md.ReadingProgress == nil || !md.ReadingProgress.Percentage.Valid { + return 0 + } + return int(md.ReadingProgress.Percentage.Float64*dur + 0.5) +} + +// AudioCurrentLabel is the playhead rendered H:MM:SS. +func (md *MediaDetail) AudioCurrentLabel() string { + s := md.AudioCurrentSeconds() + if s <= 0 { + return "0:00" + } + h := s / 3600 + m := (s % 3600) / 60 + sec := s % 60 + if h > 0 { + return fmt.Sprintf("%d:%02d:%02d", h, m, sec) + } + return fmt.Sprintf("%d:%02d", m, sec) +} + +// AudioStatsLabel is "Time listened" for audio items (H:MM:SS), honoring +// the plan's swap of the Pages Synced tile. +func (md *MediaDetail) TimeListenedLabel() string { + s := 0 + if md.BookStats != nil { + s = md.BookStats.TotalSeconds + } + h := s / 3600 + m := (s % 3600) / 60 + sec := s % 60 + if h > 0 { + return fmt.Sprintf("%d:%02d:%02d", h, m, sec) + } + return fmt.Sprintf("%d:%02d", m, sec) +} diff --git a/internal/handlers/progress.go b/internal/handlers/progress.go index 176525ad..92d6126c 100644 --- a/internal/handlers/progress.go +++ b/internal/handlers/progress.go @@ -5,6 +5,7 @@ import ( wsync "bookhoard/internal/sync" "bookhoard/internal/utils" "context" + "encoding/json" "errors" "net/http" "strconv" @@ -148,6 +149,23 @@ type ProgressWithMedia struct { LastUpdated string `json:"-"` FormatGroup string `json:"format_group"` EstimatedPages int `json:"estimated_pages"` + // Audiobook whole-book duration (seconds) for H:M:S progress display + DurationSeconds float64 `json:"duration_seconds,omitempty"` +} + +// audioDurationFrom pulls duration_seconds from the scan's audio_metadata +// JSONB (0 when the item isn't audio or hasn't been probed). +func audioDurationFrom(raw []byte) float64 { + if len(raw) == 0 { + return 0 + } + var payload struct { + DurationSeconds float64 `json:"duration_seconds"` + } + if json.Unmarshal(raw, &payload) != nil { + return 0 + } + return payload.DurationSeconds } // GetAllProgress retrieves all progress for a user with sync source info @@ -282,7 +300,10 @@ func (h *Handler) GetAllProgressData(c *echo.Context) ([]ProgressWithMedia, erro DeviceName: deviceName, DeviceType: deviceName, FormatGroup: mediaItem.FormatGroup, - EstimatedPages: wsync.EstimatedPages(mediaItem.TotalCharacters.Int64), + // Audiobook "current position" needs whole-book duration + // (H:M:S display); parsed from the scanner's audio_metadata. + DurationSeconds: audioDurationFrom(mediaItem.AudioMetadata), + EstimatedPages: wsync.EstimatedPages(mediaItem.TotalCharacters.Int64), }) } diff --git a/internal/handlers/series.go b/internal/handlers/series.go index afade132..0b820480 100644 --- a/internal/handlers/series.go +++ b/internal/handlers/series.go @@ -159,6 +159,7 @@ func (h *SeriesHandler) GetSeriesBooks(c *echo.Context) error { Title: item.Title, Author: textToString(item.Author), CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath), + FormatGroup: item.FormatGroup, }) } diff --git a/internal/router/frontend.go b/internal/router/frontend.go index ca099d2b..784a2abb 100644 --- a/internal/router/frontend.go +++ b/internal/router/frontend.go @@ -469,9 +469,22 @@ func registerFrontendRoutes(cfg *Config) { // Get only visible sections for the dashboard display visibleSections := cfg.DashboardService.FilterHiddenCollections(allSections, prefs.HiddenCollections) + // Active library type drives the per-type wording (A2c): system + // collection titles and stat labels swap on audiobooks scope; the + // "All libraries" view keeps generic wording. + activeLibType := "" + if !libRes.IsAll { + for _, ld := range libRes.Libraries { + if ld.ID == libraryID { + activeLibType = ld.TypeValue + break + } + } + } + userPgID := pgtype.UUID{Bytes: userUUID, Valid: true} - sectionData := handlers.MarkActiveConflictsSections(c.Request().Context(), cfg.Queries, userPgID, handlers.BuildSections(visibleSections, libraryID)) - allSectionsData := handlers.BuildSections(allSections, libraryID) + sectionData := handlers.MarkActiveConflictsSections(c.Request().Context(), cfg.Queries, userPgID, handlers.BuildSections(visibleSections, libraryID, activeLibType)) + allSectionsData := handlers.BuildSections(allSections, libraryID, activeLibType) // "This week" widget: the user's rolling 7-day reading pulse for // the CURRENTLY SELECTED library (the same resolution the sections @@ -502,7 +515,7 @@ func registerFrontendRoutes(cfg *Config) { } var buf bytes.Buffer - err = templates.Dashboard(user, sectionData, allSectionsData, libRes.Libraries, libraryID, prefs.HiddenCollections, limit, week, errorMsg).Render(c.Request().Context(), &buf) + err = templates.Dashboard(user, sectionData, allSectionsData, libRes.Libraries, libraryID, prefs.HiddenCollections, limit, week, activeLibType, errorMsg).Render(c.Request().Context(), &buf) if err != nil { return err } @@ -870,6 +883,15 @@ func registerFrontendRoutes(cfg *Config) { return echo.NewHTTPError(http.StatusInternalServerError, weekErr.Error()) } var buf bytes.Buffer + widgetLibType := "" + if !libRes.IsAll { + for _, ld := range libRes.Libraries { + if ld.ID == libRes.LibraryID { + widgetLibType = ld.TypeValue + break + } + } + } err = templates.DashboardWeekWidget(templates.WeekStatsData{ WeekMinutes: weekSvc.WeekMinutes, WeekPages: weekSvc.WeekPages, @@ -879,7 +901,7 @@ func registerFrontendRoutes(cfg *Config) { ActiveToday: weekSvc.ActiveToday, CurrentStreak: weekSvc.CurrentStreak, LibraryID: libRes.LibraryID, - }).Render(c.Request().Context(), &buf) + }, widgetLibType).Render(c.Request().Context(), &buf) if err != nil { return err } @@ -1672,6 +1694,7 @@ func registerFrontendRoutes(cfg *Config) { pgUserID, mediaUUID, user.Timezone); serr == nil && svcStats.SessionCount > 0 { bookStats = &handlers.BookStatsData{ TotalMinutes: svcStats.TotalMinutes, + TotalSeconds: svcStats.TotalSeconds, TotalPages: svcStats.TotalPages, SessionCount: svcStats.SessionCount, MaxProgress: svcStats.MaxProgress, diff --git a/internal/router/helpers.go b/internal/router/helpers.go index a4740a60..58a1677c 100644 --- a/internal/router/helpers.go +++ b/internal/router/helpers.go @@ -197,6 +197,7 @@ func resolveLibrary(c *echo.Context, cfg *Config, userUUID string) LibraryResolu Name: lib.Name, Description: getText(lib.Description), TypeName: lib.TypeName, + TypeValue: lib.TypeName, MediaCount: countMap[libUUID.String()], Offline: offline, } diff --git a/internal/services/media_scanner.go b/internal/services/media_scanner.go index 3aa73ffd..ddf90842 100644 --- a/internal/services/media_scanner.go +++ b/internal/services/media_scanner.go @@ -93,6 +93,12 @@ type MediaMetadata struct { // Audiobook fields (media_items.audio_metadata JSONB โ only the // audio paths read it; see media_scanner_audio.go) AudioMeta *AudioMetadataPayload + + // Narrators/abridged land in real media_items columns (A2a): the + // editorial surface of audiobooks (edit form, filters, search), + // while AudioMeta stays scan-derived. + Narrators []string + Abridged bool } type HashInfo struct { @@ -941,6 +947,9 @@ func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool, DatePublished: pgtype.Date{Time: metadata.PublishDate, Valid: !metadata.PublishDate.IsZero()}, Contributors: metadata.Contributors, ContributorsSearch: contributorsSearch, + Narrators: metadata.Narrators, + NarratorsSearch: utils.NormalizeNarratorsSearch(metadata.Narrators), + Abridged: metadata.Abridged, Tags: metadata.Tags, TagsSearch: tagsSearch, AddedByAdminID: s.adminID, @@ -1095,6 +1104,8 @@ func extractAudiobookshelfSidecar(path string) *MediaMetadata { Title string `json:"title"` Subtitle string `json:"subtitle"` Authors []string `json:"authors"` + Narrators []string `json:"narrators"` + Abridged *bool `json:"abridged"` Series json.RawMessage `json:"series"` Genres []string `json:"genres"` Tags []string `json:"tags"` @@ -1173,6 +1184,14 @@ func extractAudiobookshelfSidecar(path string) *MediaMetadata { if sidecar.ASIN != nil { metadata.ASIN = strings.TrimSpace(*sidecar.ASIN) } + // Audiobook editorial facts land in the real columns (A2a): + // narrators verbatim (display), abridged as the Libation/Audible flag. + if len(sidecar.Narrators) > 0 { + metadata.Narrators = sidecar.Narrators + } + if sidecar.Abridged != nil { + metadata.Abridged = *sidecar.Abridged + } if sidecar.Language != nil { metadata.Language = strings.TrimSpace(*sidecar.Language) } @@ -3191,6 +3210,9 @@ func (s *MediaScanner) updateMediaItem(ctx context.Context, existing database.Me 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}, + Narrators: metadata.Narrators, + NarratorsSearch: utils.NormalizeNarratorsSearch(metadata.Narrators), + Abridged: metadata.Abridged, Tags: metadata.Tags, TagsSearch: tagsSearch, Asin: pgtype.Text{String: metadata.ASIN, Valid: metadata.ASIN != ""}, diff --git a/internal/services/media_scanner_audio.go b/internal/services/media_scanner_audio.go index 65528266..012000e3 100644 --- a/internal/services/media_scanner_audio.go +++ b/internal/services/media_scanner_audio.go @@ -42,6 +42,7 @@ type AudioMetadataPayload struct { CodecProfile string `json:"codec_profile,omitempty"` BitRate int64 `json:"bit_rate,omitempty"` ASIN string `json:"asin,omitempty"` + Abridged *bool `json:"abridged,omitempty"` Chapters []AudioChapter `json:"chapters,omitempty"` } @@ -182,14 +183,24 @@ func (s *MediaScanner) enrichAudioMetadata(path string, metadata *MediaMetadata) if payload.ASIN == "" { payload.ASIN = metadata.ASIN } + + // Promote the editorial facts (A2a columns) when the sidecar's audio + // payload carries them and the common-field parse left them empty. + if len(metadata.Narrators) == 0 && len(payload.Narrators) > 0 { + metadata.Narrators = payload.Narrators + } + if payload.Abridged != nil { + metadata.Abridged = *payload.Abridged + } } // parseSidecarAudioFields extracts the audio-only fields from a -// Libation/Audiobookshelf metadata.json into the payload (narrators and -// the curated chapter list). +// Libation/Audiobookshelf metadata.json into the payload (narrators, +// abridged and the curated chapter list). func parseSidecarAudioFields(path string, data []byte) *AudioMetadataPayload { var sidecar struct { Narrators []string `json:"narrators"` + Abridged *bool `json:"abridged"` ASIN *string `json:"asin"` Chapters []struct { Start float64 `json:"start"` @@ -201,11 +212,12 @@ func parseSidecarAudioFields(path string, data []byte) *AudioMetadataPayload { if err := json.Unmarshal(data, &sidecar); err != nil { return nil } - if len(sidecar.Narrators) == 0 && len(sidecar.Chapters) == 0 && sidecar.ASIN == nil { + if len(sidecar.Narrators) == 0 && len(sidecar.Chapters) == 0 && sidecar.ASIN == nil && sidecar.Abridged == nil { return nil } payload := &AudioMetadataPayload{ Narrators: sidecar.Narrators, + Abridged: sidecar.Abridged, } if sidecar.ASIN != nil { payload.ASIN = strings.TrimSpace(*sidecar.ASIN) diff --git a/internal/services/search.go b/internal/services/search.go index edf3ad4b..a6277fd7 100644 --- a/internal/services/search.go +++ b/internal/services/search.go @@ -29,6 +29,7 @@ type SearchParams struct { GenreFilter string PublisherFilter string TagsFilter string + NarratorFilter string LanguageFilter string YearMin int YearMax int @@ -76,6 +77,7 @@ func (s *SearchService) SearchMediaItemsUnified(ctx context.Context, params Sear GenreFilter: pgtype.Text{String: params.GenreFilter, Valid: true}, PublisherFilter: pgtype.Text{String: params.PublisherFilter, Valid: true}, TagsFilter: pgtype.Text{String: params.TagsFilter, Valid: true}, + NarratorFilter: pgtype.Text{String: params.NarratorFilter, Valid: true}, LanguageFilter: pgtype.Text{String: params.LanguageFilter, Valid: true}, YearMin: pgtype.Int4{Int32: int32(params.YearMin), Valid: true}, YearMax: pgtype.Int4{Int32: int32(params.YearMax), Valid: true}, @@ -174,6 +176,23 @@ func (s *SearchService) SearchFieldValues(ctx context.Context, params FieldSearc } return fieldValues, nil + case "narrator": + results, err := s.db.SearchNarratorValues(ctx, database.SearchNarratorValuesParams{ + SearchQuery: pgtype.Text{String: params.SearchQuery, Valid: true}, + UserID: params.UserID, + LibraryID: params.LibraryID, + Limit: pgtype.Int4{Int32: int32(params.Limit), Valid: true}, + Offset: pgtype.Int4{Int32: int32(params.Offset), Valid: true}, + }) + if err != nil { + return nil, err + } + fieldValues := make([]FieldValue, len(results)) + for i, r := range results { + fieldValues[i] = FieldValue{Value: r.Value, Count: r.Count, Score: r.Score} + } + return fieldValues, nil + case "series": results, err := s.db.SearchSeriesValues(ctx, database.SearchSeriesValuesParams{ SearchQuery: pgtype.Text{String: params.SearchQuery, Valid: true}, diff --git a/internal/services/statistics_service.go b/internal/services/statistics_service.go index 713b6665..1eef3b91 100644 --- a/internal/services/statistics_service.go +++ b/internal/services/statistics_service.go @@ -163,6 +163,7 @@ type WeekSummaryStats struct { type BookStats struct { MediaItemID string `json:"media_item_id"` TotalMinutes int `json:"total_minutes"` + TotalSeconds int `json:"total_seconds"` // exact wall-clock (audio "Time listened"; minutes lose the remainder) TotalPages int `json:"total_pages"` SessionCount int `json:"session_count"` MaxProgress float64 `json:"max_progress"` @@ -542,6 +543,7 @@ func (s *StatisticsService) BookStats(ctx context.Context, userID pgtype.UUID, m } for i, sess := range sessions { if sess.TimeSpentSeconds.Valid { + out.TotalSeconds += int(sess.TimeSpentSeconds.Int32) out.TotalMinutes += int(sess.TimeSpentSeconds.Int32) / 60 } if sess.PagesRead.Valid { diff --git a/internal/utils/metadata_overrides.go b/internal/utils/metadata_overrides.go index aa0f6f13..4d3b0bf4 100644 --- a/internal/utils/metadata_overrides.go +++ b/internal/utils/metadata_overrides.go @@ -23,6 +23,8 @@ const ( OverrideDescription = "description" OverrideCoverImagePath = "cover_image_path" OverrideSeries = "series" + OverrideNarrators = "narrators" + OverrideAbridged = "abridged" OverrideSeriesNumber = "series_number" OverrideTags = "tags" OverrideAsin = "asin" @@ -154,6 +156,8 @@ func detectMetadataOverridesDiff(params database.UpdateMediaItemParams, existing add(OverrideSeries, normText(params.Series) != normText(existing.Series)) add(OverrideSeriesNumber, normInt(params.SeriesNumber) != normInt(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(OverrideAsin, normText(params.Asin) != normText(existing.Asin)) add(OverrideDatePublished, normDate(params.DatePublished) != normDate(existing.DatePublished)) add(OverridePublisher, normText(params.Publisher) != normText(existing.Publisher)) @@ -223,6 +227,13 @@ func ApplyMetadataOverrides(params *database.UpdateMediaItemParams, existing dat params.Tags = existing.Tags params.TagsSearch = existing.TagsSearch } + if hasOverride(o, OverrideNarrators) { + params.Narrators = existing.Narrators + params.NarratorsSearch = existing.NarratorsSearch + } + if hasOverride(o, OverrideAbridged) { + params.Abridged = existing.Abridged + } if hasOverride(o, OverrideAsin) { params.Asin = existing.Asin } diff --git a/internal/utils/tags.go b/internal/utils/tags.go index 9e2235fa..3e83b59e 100644 --- a/internal/utils/tags.go +++ b/internal/utils/tags.go @@ -176,6 +176,13 @@ func NormalizeTagsSearch(tags []string) []string { return normalized } +// NormalizeNarratorsSearch mirrors NormalizeTagsSearch for narrator names โ +// the trigram-search TEXT[] paired with media_items.narrators (filters and +// free-text search hit this). +func NormalizeNarratorsSearch(narrators []string) []string { + return NormalizeTagsSearch(narrators) +} + // NormalizeContributors normalizes an array of contributors for display: // 1. Trim whitespace // 2. Preserve original casing (including CAPSLOCK companies) diff --git a/templates/audio_reader.templ b/templates/audio_reader.templ index d2d7c508..837710c6 100644 --- a/templates/audio_reader.templ +++ b/templates/audio_reader.templ @@ -33,134 +33,283 @@ templ AudioReader(user User, metadata ReaderMetadata) { "/api/media-items/"+metadata.MediaItemID+"/progress") } >
โ
-โ
+- No chapters detected for this audiobook. -
+ No chapters detected for this audiobook. +
++ No bookmarks yet โ tap the button to save your position. +
+Bookmark at 0:00
+ +