diff --git a/database/schema/schema.sql b/database/schema/schema.sql index f1e53b0..d7a1cba 100644 --- a/database/schema/schema.sql +++ b/database/schema/schema.sql @@ -207,6 +207,12 @@ CREATE TABLE IF NOT EXISTS media_items ( -- Summary (distinct from description - may merge with Calibre description) summary TEXT, -- Summary from ComicInfo.xml (may be merged with description from Calibre) + -- Per-field user-override tracking. Column names listed here (e.g. + -- 'description', 'tags', 'cover_image_path') are user-customized and MUST + -- NOT be overwritten by library scans or per-book rescans; only the + -- reset-to-scanned-defaults action clears them. + metadata_overrides TEXT[] NOT NULL DEFAULT '{}', + -- Chapter metadata for reader navigation and progress tracking -- Caches detected chapter structure to avoid re-parsing files -- Populated by ReaderService.DetectChapters() on first read @@ -232,6 +238,11 @@ 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[]; +-- 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) +ALTER TABLE media_items ADD COLUMN IF NOT EXISTS metadata_overrides TEXT[] NOT NULL DEFAULT '{}'; + -- 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); diff --git a/internal/database/db.go b/internal/database/db.go index bdf4241..486aa36 100644 --- a/internal/database/db.go +++ b/internal/database/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.1 package database diff --git a/internal/database/models.go b/internal/database/models.go index 5355e97..5af8871 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.1 package database @@ -306,6 +306,7 @@ type MediaItems struct { ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` // Summary from ComicInfo.xml (may be merged with description from Calibre) Summary pgtype.Text `db:"summary" json:"summary"` + MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` TagsSearch []string `db:"tags_search" json:"tags_search"` diff --git a/internal/database/querier.go b/internal/database/querier.go index 2a54c98..4ae6b0a 100644 --- a/internal/database/querier.go +++ b/internal/database/querier.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.1 package database @@ -30,6 +30,9 @@ type Querier interface { ClearDeviceSyncQueue(ctx context.Context, deviceID pgtype.UUID) error ClearKoboShelf(ctx context.Context, deviceID pgtype.UUID) error ClearKoboShelfByName(ctx context.Context, arg ClearKoboShelfByNameParams) error + // Reset an item to scanned defaults: clears per-field user overrides so the + // next rescan can freely overwrite user-customized metadata. + ClearMediaItemMetadataOverrides(ctx context.Context, id pgtype.UUID) error CountAdmins(ctx context.Context) (int64, error) // Count unlinked books for a device CountUnlinkedBooks(ctx context.Context, deviceID pgtype.UUID) (int64, error) diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index c16f6c7..18b8576 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.30.0 +// sqlc v1.31.1 // source: queries.sql package database @@ -226,6 +226,18 @@ func (q *Queries) ClearKoboShelfByName(ctx context.Context, arg ClearKoboShelfBy return err } +const ClearMediaItemMetadataOverrides = `-- name: ClearMediaItemMetadataOverrides :exec +UPDATE media_items SET metadata_overrides = '{}', updated_at = NOW() +WHERE id = $1 +` + +// Reset an item to scanned defaults: clears per-field user overrides so the +// next rescan can freely overwrite user-customized metadata. +func (q *Queries) ClearMediaItemMetadataOverrides(ctx context.Context, id pgtype.UUID) error { + _, err := q.db.Exec(ctx, ClearMediaItemMetadataOverrides, id) + return err +} + const CountAdmins = `-- name: CountAdmins :one SELECT COUNT(*) FROM users WHERE role = 'admin' ` @@ -900,7 +912,7 @@ 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) 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) 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, chapter_metadata, library_type_name, 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, 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, chapter_metadata, library_type_name, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence ` type CreateMediaItemParams struct { @@ -1053,6 +1065,7 @@ func (q *Queries) CreateMediaItem(ctx context.Context, arg CreateMediaItemParams &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -2447,7 +2460,7 @@ func (q *Queries) GetAnnotationsForBook(ctx context.Context, arg GetAnnotationsF } 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, chapter_metadata, library_type_name, 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, 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, chapter_metadata, library_type_name, tags_search, contributors_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 ` @@ -2520,6 +2533,7 @@ func (q *Queries) GetBooksByTag(ctx context.Context, arg GetBooksByTagParams) ([ &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -2619,7 +2633,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.chapter_metadata, mi.library_type_name, 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.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.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_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) @@ -2687,6 +2701,7 @@ type GetCollectionItemsForDashboardRow struct { AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` Summary pgtype.Text `db:"summary" json:"summary"` + MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` TagsSearch []string `db:"tags_search" json:"tags_search"` @@ -2761,6 +2776,7 @@ func (q *Queries) GetCollectionItemsForDashboard(ctx context.Context, arg GetCol &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -2913,7 +2929,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.chapter_metadata, mi.library_type_name, 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.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.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_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 @@ -2997,6 +3013,7 @@ func (q *Queries) GetContinueReadingItems(ctx context.Context, arg GetContinueRe &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -3030,7 +3047,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.chapter_metadata, mi.library_type_name, 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.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.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_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 @@ -3038,7 +3055,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, chapter_metadata, library_type_name, 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, 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, chapter_metadata, library_type_name, tags_search, contributors_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 ` @@ -3103,6 +3120,7 @@ type GetContinueSeriesItemsRow struct { AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` Summary pgtype.Text `db:"summary" json:"summary"` + MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` TagsSearch []string `db:"tags_search" json:"tags_search"` @@ -3177,6 +3195,7 @@ func (q *Queries) GetContinueSeriesItems(ctx context.Context, arg GetContinueSer &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -4306,7 +4325,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.chapter_metadata, mi.library_type_name, 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.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.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_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) ORDER BY mi.created_at DESC ` @@ -4374,6 +4393,7 @@ func (q *Queries) GetLibraryItems(ctx context.Context, libraryID pgtype.UUID) ([ &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -4805,7 +4825,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, chapter_metadata, library_type_name, 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, 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, chapter_metadata, library_type_name, tags_search, contributors_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) { @@ -4865,6 +4885,7 @@ func (q *Queries) GetMediaItem(ctx context.Context, id pgtype.UUID) (MediaItems, &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -4878,7 +4899,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, chapter_metadata, library_type_name, 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, 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, chapter_metadata, library_type_name, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_path = $1 AND library_id = $2 ` type GetMediaItemByFilePathParams struct { @@ -4943,6 +4964,7 @@ func (q *Queries) GetMediaItemByFilePath(ctx context.Context, arg GetMediaItemBy &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -4956,7 +4978,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, chapter_metadata, library_type_name, 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, 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, chapter_metadata, library_type_name, tags_search, contributors_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) { @@ -5016,6 +5038,7 @@ func (q *Queries) GetMediaItemByFilePathAnyLibrary(ctx context.Context, filePath &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -5030,7 +5053,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, chapter_metadata, library_type_name, 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, 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, chapter_metadata, library_type_name, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_path = $1 ` // ============================================ @@ -5093,6 +5116,7 @@ func (q *Queries) GetMediaItemByFilePathForSync(ctx context.Context, filePath st &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -5106,7 +5130,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, chapter_metadata, library_type_name, 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, 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, chapter_metadata, library_type_name, tags_search, contributors_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) { @@ -5166,6 +5190,7 @@ func (q *Queries) GetMediaItemByKoboContentId(ctx context.Context, koboContentID &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -5179,7 +5204,7 @@ func (q *Queries) GetMediaItemByKoboContentId(ctx context.Context, koboContentID } 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, chapter_metadata, library_type_name, 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, 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, chapter_metadata, library_type_name, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE opf_identifier = $1 ` // Get media item by OPF identifier @@ -5240,6 +5265,7 @@ func (q *Queries) GetMediaItemByOPFIdentifier(ctx context.Context, opfIdentifier &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -5253,7 +5279,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, chapter_metadata, library_type_name, 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, 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, chapter_metadata, library_type_name, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE opf_uuid = $1 ` // Get media item by OPF UUID @@ -5314,6 +5340,7 @@ func (q *Queries) GetMediaItemByOPFUUID(ctx context.Context, opfUuid pgtype.Text &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -5327,7 +5354,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, chapter_metadata, library_type_name, 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, 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, chapter_metadata, library_type_name, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_sha256 = $1 ` // Get media item by SHA-256 hash @@ -5388,6 +5415,7 @@ func (q *Queries) GetMediaItemBySHA256(ctx context.Context, fileSha256 pgtype.Te &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -5401,7 +5429,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, chapter_metadata, library_type_name, 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, 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, chapter_metadata, library_type_name, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_sha256 = $1 AND library_id = $2 ` type GetMediaItemBySHA256AndLibraryParams struct { @@ -5467,6 +5495,7 @@ func (q *Queries) GetMediaItemBySHA256AndLibrary(ctx context.Context, arg GetMed &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -5807,7 +5836,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.chapter_metadata, mi.library_type_name, 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.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.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_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 NOT EXISTS ( SELECT 1 FROM reading_progress rp @@ -5888,6 +5917,7 @@ func (q *Queries) GetNotStartedItems(ctx context.Context, arg GetNotStartedItems &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -6207,7 +6237,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.chapter_metadata, mi.library_type_name, 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.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.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_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) ORDER BY mi.imported_at DESC NULLS LAST, mi.created_at DESC LIMIT $2 @@ -6281,6 +6311,7 @@ func (q *Queries) GetRecentlyAddedItems(ctx context.Context, arg GetRecentlyAdde &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -6301,7 +6332,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.chapter_metadata, mi.library_type_name, 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.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.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_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 @@ -6383,6 +6414,7 @@ func (q *Queries) GetRecentlyReadItems(ctx context.Context, arg GetRecentlyReadI &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -6503,7 +6535,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, chapter_metadata, library_type_name, 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, 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, chapter_metadata, library_type_name, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE series = $1 ORDER BY series_number ASC NULLS LAST ` @@ -6571,6 +6603,7 @@ func (q *Queries) GetSeriesBooks(ctx context.Context, series pgtype.Text) ([]Med &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -8354,7 +8387,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.chapter_metadata, mi.library_type_name, 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.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.chapter_metadata, mi.library_type_name, 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 FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id @@ -8420,6 +8453,7 @@ type ListMediaItemsRow struct { AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` Summary pgtype.Text `db:"summary" json:"summary"` + MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` TagsSearch []string `db:"tags_search" json:"tags_search"` @@ -8495,6 +8529,7 @@ func (q *Queries) ListMediaItems(ctx context.Context, arg ListMediaItemsParams) &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -8517,7 +8552,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.chapter_metadata, mi.library_type_name, 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.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.chapter_metadata, mi.library_type_name, 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 FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id @@ -8579,6 +8614,7 @@ type ListMediaItemsByLibraryRow struct { AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` Summary pgtype.Text `db:"summary" json:"summary"` + MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` TagsSearch []string `db:"tags_search" json:"tags_search"` @@ -8654,6 +8690,7 @@ func (q *Queries) ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype. &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -8676,7 +8713,7 @@ func (q *Queries) ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype. } 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, chapter_metadata, library_type_name, 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, 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, chapter_metadata, library_type_name, 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 ` type ListMediaItemsBySHA256AndLibraryParams struct { @@ -8748,6 +8785,7 @@ func (q *Queries) ListMediaItemsBySHA256AndLibrary(ctx context.Context, arg List &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -8768,7 +8806,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, chapter_metadata, library_type_name, 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, 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, chapter_metadata, library_type_name, tags_search, contributors_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) @@ -8835,6 +8873,7 @@ func (q *Queries) ListMediaItemsMissingHash(ctx context.Context) ([]MediaItems, &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -8855,7 +8894,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.chapter_metadata, mi.library_type_name, 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.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.chapter_metadata, mi.library_type_name, 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 FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id @@ -8990,6 +9029,7 @@ type ListMediaItemsSortedRow struct { AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` Summary pgtype.Text `db:"summary" json:"summary"` + MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` TagsSearch []string `db:"tags_search" json:"tags_search"` @@ -9070,6 +9110,7 @@ func (q *Queries) ListMediaItemsSorted(ctx context.Context, arg ListMediaItemsSo &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -10193,7 +10234,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.chapter_metadata, mi.library_type_name, 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.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.chapter_metadata, mi.library_type_name, 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 FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id @@ -10281,6 +10322,7 @@ type SearchMediaItemsRow struct { AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` Summary pgtype.Text `db:"summary" json:"summary"` + MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` TagsSearch []string `db:"tags_search" json:"tags_search"` @@ -10363,6 +10405,7 @@ func (q *Queries) SearchMediaItems(ctx context.Context, arg SearchMediaItemsPara &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -11834,9 +11877,10 @@ UPDATE media_items SET alternate_info = $35, scan_information = $36, summary = $37, + metadata_overrides = $38, 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, chapter_metadata, library_type_name, 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, 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, chapter_metadata, library_type_name, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence ` type UpdateMediaItemParams struct { @@ -11877,6 +11921,7 @@ type UpdateMediaItemParams struct { AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` Summary pgtype.Text `db:"summary" json:"summary"` + MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` } func (q *Queries) UpdateMediaItem(ctx context.Context, arg UpdateMediaItemParams) (MediaItems, error) { @@ -11918,6 +11963,7 @@ func (q *Queries) UpdateMediaItem(ctx context.Context, arg UpdateMediaItemParams arg.AlternateInfo, arg.ScanInformation, arg.Summary, + arg.MetadataOverrides, ) var i MediaItems err := row.Scan( @@ -11974,6 +12020,7 @@ func (q *Queries) UpdateMediaItem(ctx context.Context, arg UpdateMediaItemParams &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -11990,7 +12037,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, chapter_metadata, library_type_name, 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, 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, chapter_metadata, library_type_name, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence ` type UpdateMediaItemChapterMetadataParams struct { @@ -12055,6 +12102,7 @@ func (q *Queries) UpdateMediaItemChapterMetadata(ctx context.Context, arg Update &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -12162,7 +12210,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, chapter_metadata, library_type_name, 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, 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, chapter_metadata, library_type_name, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence ` type UpdateMediaItemIdentifiersParams struct { @@ -12241,6 +12289,7 @@ func (q *Queries) UpdateMediaItemIdentifiers(ctx context.Context, arg UpdateMedi &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -12261,7 +12310,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, chapter_metadata, library_type_name, 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, 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, chapter_metadata, library_type_name, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence ` type UpdateMediaItemKoboMetadataParams struct { @@ -12335,6 +12384,7 @@ func (q *Queries) UpdateMediaItemKoboMetadata(ctx context.Context, arg UpdateMed &i.AlternateInfo, &i.ScanInformation, &i.Summary, + &i.MetadataOverrides, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, diff --git a/internal/database/queries/queries.sql b/internal/database/queries/queries.sql index 4c33b93..6586edc 100644 --- a/internal/database/queries/queries.sql +++ b/internal/database/queries/queries.sql @@ -282,10 +282,17 @@ UPDATE media_items SET alternate_info = $35, scan_information = $36, summary = $37, + metadata_overrides = $38, updated_at = NOW() WHERE id = $1 RETURNING *; +-- name: ClearMediaItemMetadataOverrides :exec +-- Reset an item to scanned defaults: clears per-field user overrides so the +-- next rescan can freely overwrite user-customized metadata. +UPDATE media_items SET metadata_overrides = '{}', updated_at = NOW() +WHERE id = $1; + -- name: DeleteMediaItem :exec DELETE FROM media_items WHERE id = $1; diff --git a/internal/handlers/media.go b/internal/handlers/media.go index e899940..5b4d677 100644 --- a/internal/handlers/media.go +++ b/internal/handlers/media.go @@ -63,41 +63,41 @@ type CreateMediaItemRequest struct { // UpdateMediaItemRequest represents the request for updating a media item type UpdateMediaItemRequest struct { - Title string `form:"title" json:"title" validate:"required,min=1,max=500"` - Author string `form:"author" json:"author"` - ISBN string `form:"isbn" json:"isbn"` - Description string `form:"description" json:"description"` - CoverImagePath string `form:"cover_image_path" json:"cover_image_path"` - CoverAction string `form:"cover_action" json:"cover_action"` - Series string `form:"series" json:"series"` - SeriesNumber int32 `form:"series_number" json:"series_number"` - Tags []string `form:"tags" json:"tags"` - ASIN string `form:"asin" json:"asin"` - DatePublished string `form:"date_published" json:"date_published"` - Publisher string `form:"publisher" json:"publisher"` - Contributors []string `form:"contributors" json:"contributors"` - Language string `form:"language" json:"language"` - Edition string `form:"edition" json:"edition"` - PageCount int32 `form:"page_count" json:"page_count"` - Genre string `form:"genre" json:"genre"` - CopyrightYear int32 `form:"copyright_year" json:"copyright_year"` - GoodreadsID string `form:"goodreads_id" json:"goodreads_id"` - OpenlibraryID string `form:"openlibrary_id" json:"openlibrary_id"` - GoogleBooksID string `form:"google_books_id" json:"google_books_id"` - MangaType string `form:"manga_type" json:"manga_type"` - ReadingDirection string `form:"reading_direction" json:"reading_direction"` - SeriesCount int32 `form:"series_count" json:"series_count"` - Volume int32 `form:"volume" json:"volume"` - Imprint string `form:"imprint" json:"imprint"` - AgeRating string `form:"age_rating" json:"age_rating"` - WebURL string `form:"web_url" json:"web_url"` - MetadataNotes string `form:"metadata_notes" json:"metadata_notes"` - CommunityRating float64 `form:"community_rating" json:"community_rating"` - StoryArc string `form:"story_arc" json:"story_arc"` - IsBlackAndWhite bool `form:"is_black_and_white" json:"is_black_and_white"` - AlternateInfo string `form:"alternate_info" json:"alternate_info"` - ScanInformation string `form:"scan_information" json:"scan_information"` - Summary string `form:"summary" json:"summary"` + Title string `form:"title" json:"title" validate:"required,min=1,max=500"` + Author string `form:"author" json:"author"` + ISBN string `form:"isbn" json:"isbn"` + Description string `form:"description" json:"description"` + CoverImagePath string `form:"cover_image_path" json:"cover_image_path"` + CoverAction string `form:"cover_action" json:"cover_action"` + Series string `form:"series" json:"series"` + SeriesNumber int32 `form:"series_number" json:"series_number"` + Tags []string `form:"tags" json:"tags"` + ASIN string `form:"asin" json:"asin"` + DatePublished string `form:"date_published" json:"date_published"` + Publisher string `form:"publisher" json:"publisher"` + Contributors []string `form:"contributors" json:"contributors"` + Language string `form:"language" json:"language"` + Edition string `form:"edition" json:"edition"` + PageCount int32 `form:"page_count" json:"page_count"` + Genre string `form:"genre" json:"genre"` + CopyrightYear int32 `form:"copyright_year" json:"copyright_year"` + GoodreadsID string `form:"goodreads_id" json:"goodreads_id"` + OpenlibraryID string `form:"openlibrary_id" json:"openlibrary_id"` + GoogleBooksID string `form:"google_books_id" json:"google_books_id"` + MangaType string `form:"manga_type" json:"manga_type"` + ReadingDirection string `form:"reading_direction" json:"reading_direction"` + SeriesCount int32 `form:"series_count" json:"series_count"` + Volume int32 `form:"volume" json:"volume"` + Imprint string `form:"imprint" json:"imprint"` + AgeRating string `form:"age_rating" json:"age_rating"` + WebURL string `form:"web_url" json:"web_url"` + MetadataNotes string `form:"metadata_notes" json:"metadata_notes"` + CommunityRating float64 `form:"community_rating" json:"community_rating"` + StoryArc string `form:"story_arc" json:"story_arc"` + IsBlackAndWhite bool `form:"is_black_and_white" json:"is_black_and_white"` + AlternateInfo string `form:"alternate_info" json:"alternate_info"` + ScanInformation string `form:"scan_information" json:"scan_information"` + Summary string `form:"summary" json:"summary"` } // CreateMediaNoteRequest represents the request for creating a media note @@ -572,24 +572,33 @@ func (h *MediaHandler) HandleBulkUpdate(c *echo.Context) error { Summary: existingMedia.Summary, } + // Bulk edits are user customizations: keep existing overrides and mark + // each applied update as overridden so scans preserve it. + overrides := existingMedia.MetadataOverrides if update.Updates.Title != nil { updateParams.Title = *update.Updates.Title + overrides = utils.MergeOverrides(overrides, utils.OverrideTitle) } if update.Updates.Author != nil { updateParams.Author = pgtype.Text{String: *update.Updates.Author, Valid: true} + overrides = utils.MergeOverrides(overrides, utils.OverrideAuthor) } if update.Updates.Genre != nil { updateParams.Genre = pgtype.Text{String: *update.Updates.Genre, Valid: true} + overrides = utils.MergeOverrides(overrides, utils.OverrideGenre) } if update.Updates.Language != nil { updateParams.Language = pgtype.Text{String: *update.Updates.Language, Valid: true} + overrides = utils.MergeOverrides(overrides, utils.OverrideLanguage) } if len(update.Updates.Tags) > 0 { normalizedTags := utils.NormalizeTags(update.Updates.Tags) updateParams.Tags = normalizedTags tagsSearch := utils.NormalizeTagsSearch(update.Updates.Tags) updateParams.TagsSearch = tagsSearch + overrides = utils.MergeOverrides(overrides, utils.OverrideTags) } + updateParams.MetadataOverrides = overrides _, err = h.db.UpdateMediaItem(c.Request().Context(), updateParams) if err != nil { @@ -925,22 +934,22 @@ func (mh *MediaHandler) GetMediaReadingProgress(c *echo.Context) error { } resp := map[string]interface{}{ - "id": progress.ID, - "media_item_id": progress.MediaItemID, - "user_id": progress.UserID, - "current_page": progress.CurrentPage, - "total_pages": progress.TotalPages, - "last_read_at": progress.LastReadAt, - "percentage": progress.Percentage, - "character_offset": progress.CharacterOffset, - "epubcfi": progress.Epubcfi, - "chapter": progress.Chapter, - "chapter_progress": progress.ChapterProgress, - "format_group": progress.FormatGroup, - "total_characters": progress.TotalCharacters, - "chapter_count": progress.ChapterCount, - "last_sync_device": progress.LastSyncDevice, - "last_sync_source": progress.LastSyncSource, + "id": progress.ID, + "media_item_id": progress.MediaItemID, + "user_id": progress.UserID, + "current_page": progress.CurrentPage, + "total_pages": progress.TotalPages, + "last_read_at": progress.LastReadAt, + "percentage": progress.Percentage, + "character_offset": progress.CharacterOffset, + "epubcfi": progress.Epubcfi, + "chapter": progress.Chapter, + "chapter_progress": progress.ChapterProgress, + "format_group": progress.FormatGroup, + "total_characters": progress.TotalCharacters, + "chapter_count": progress.ChapterCount, + "last_sync_device": progress.LastSyncDevice, + "last_sync_source": progress.LastSyncSource, "last_sync_timestamp": progress.LastSyncTimestamp, } @@ -1240,6 +1249,7 @@ func (mh *MediaHandler) UpdateMediaItem(c *echo.Context) error { } coverPath := existing.CoverImagePath.String + coverUploaded := false if req.CoverAction == "remove" { coverPath = "" @@ -1251,6 +1261,7 @@ func (mh *MediaHandler) UpdateMediaItem(c *echo.Context) error { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to save cover image"}) } coverPath = savedPath + coverUploaded = true } } @@ -1259,7 +1270,7 @@ func (mh *MediaHandler) UpdateMediaItem(c *echo.Context) error { alternateInfoBytes = []byte(req.AlternateInfo) } - item, err := mh.db.UpdateMediaItem(c.Request().Context(), database.UpdateMediaItemParams{ + params := database.UpdateMediaItemParams{ ID: pgtype.UUID{Bytes: mediaUUID, Valid: true}, Title: req.Title, Author: pgtype.Text{String: req.Author, Valid: req.Author != ""}, @@ -1297,7 +1308,17 @@ func (mh *MediaHandler) UpdateMediaItem(c *echo.Context) error { AlternateInfo: alternateInfoBytes, ScanInformation: pgtype.Text{String: req.ScanInformation, Valid: req.ScanInformation != ""}, Summary: pgtype.Text{String: req.Summary, Valid: req.Summary != ""}, - }) + } + + // Fields the user actually changed become overrides so future scans keep + // the custom values. An explicit cover upload/removal always overrides. + overrides := utils.DetectMetadataOverrides(params, existing) + if req.CoverAction == "remove" || coverUploaded { + overrides = utils.MergeOverrides(overrides, utils.OverrideCoverImagePath) + } + params.MetadataOverrides = overrides + + item, err := mh.db.UpdateMediaItem(c.Request().Context(), params) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } @@ -1333,7 +1354,11 @@ func (mh *MediaHandler) RescanMediaItem(c *echo.Context) error { scanner := services.NewMediaScanner(mh.db) defer scanner.Close() - if err := scanner.RescanMediaItem(c.Request().Context(), pgtype.UUID{Bytes: mediaUUID, Valid: true}); err != nil { + // reset_overrides=true discards user customizations first, returning the + // item to pure scanned defaults (the "Reset to Scanned" action). + resetOverrides := c.QueryParam("reset_overrides") == "true" + + if err := scanner.RescanMediaItem(c.Request().Context(), pgtype.UUID{Bytes: mediaUUID, Valid: true}, resetOverrides); err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } @@ -2224,7 +2249,17 @@ func (mh *MediaHandler) saveCoverImage(c echo.Context, mediaUUID uuid.UUID, file return "", fmt.Errorf("media item has no file path") } - coverRelPath := relativeFilePath + ".cover.jpg" + // Custom covers live at a dedicated sidecar path (distinct from the + // scanner-generated {file}.cover.jpg) so scans can never overwrite a + // user-uploaded cover and the metadata_overrides set can protect it. + ext := ".jpg" + switch contentType { + case "image/png": + ext = ".png" + case "image/webp": + ext = ".webp" + } + coverRelPath := relativeFilePath + ".custom_cover" + ext coverFullPath, err := mh.libraryService.ResolveMediaPath(c.Request().Context(), mediaItem.LibraryID, coverRelPath) if err != nil { diff --git a/internal/services/media_scanner.go b/internal/services/media_scanner.go index 607cf8b..c04e721 100644 --- a/internal/services/media_scanner.go +++ b/internal/services/media_scanner.go @@ -157,18 +157,18 @@ type CalibreOPFMetadata struct { // never closed. func NewMediaScanner(db *database.Queries) *MediaScanner { return &MediaScanner{ - db: db, - watcher: nil, - settingsCache: NewSettingsCache(30 * time.Second), - dirtyDirs: make(map[string]time.Time), - fileStability: make(map[string]*atomic.Bool), - watching: atomic.Bool{}, - scanInProgress: atomic.Bool{}, - folders: []string{}, - adminID: pgtype.UUID{}, + db: db, + watcher: nil, + settingsCache: NewSettingsCache(30 * time.Second), + dirtyDirs: make(map[string]time.Time), + fileStability: make(map[string]*atomic.Bool), + watching: atomic.Bool{}, + scanInProgress: atomic.Bool{}, + folders: []string{}, + adminID: pgtype.UUID{}, defaultLibraryID: pgtype.UUID{Valid: false}, - libraryTypes: make(map[string][]string), - logger: NewScannerLogger(), + libraryTypes: make(map[string][]string), + logger: NewScannerLogger(), } } @@ -697,7 +697,7 @@ func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool, if s.forceRescan { fmt.Printf("Force rescan enabled, updating existing media item: %s\n", path) // Use UPDATE instead of DELETE+INSERT to preserve created_at - if err := s.updateMediaItem(ctx, existingItem.ID, path, info); err != nil { + if err := s.updateMediaItem(ctx, existingItem, path); err != nil { fmt.Printf("Warning: failed to update existing media item: %v\n", err) } // Recompute hash identifiers too - a force rescan is the admin's @@ -708,7 +708,7 @@ func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool, // Normal behavior: check if file has changed (by size) if existingItem.FileSize.Int64 != info.Size() { fmt.Printf("File size changed, updating media item: %s\n", path) - _ = s.updateMediaItem(ctx, existingItem.ID, path, info) + _ = s.updateMediaItem(ctx, existingItem, path) // The bytes changed, so any stored hash is stale. s.recomputeHashInfo(ctx, existingItem.ID, libraryID, path) return false, nil @@ -758,7 +758,7 @@ func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool, fmt.Printf("Media item with same SHA-256 already exists in library (path %q), skipping duplicate: %s\n", existingByHash.FilePath, path) if s.forceRescan { - _ = s.updateMediaItem(ctx, existingByHash.ID, path, info) + _ = s.updateMediaItem(ctx, existingByHash, path) } return false, nil } else if err != nil && !errors.Is(err, pgx.ErrNoRows) { @@ -2605,7 +2605,11 @@ func countArchiveImages(filePath string) (int, error) { return count, nil } -func (s *MediaScanner) updateMediaItem(ctx context.Context, mediaItemID pgtype.UUID, path string, _ os.FileInfo) error { +// updateMediaItem re-extracts metadata for an existing media item and writes +// it back. Fields listed in the item's metadata_overrides set are preserved +// from the existing row so rescans never clobber user customizations; only +// reset-to-scanned-defaults (RescanMediaItem with reset) clears them. +func (s *MediaScanner) updateMediaItem(ctx context.Context, existing database.MediaItems, path string) error { // Re-extract metadata for the update metadata, err := s.extractMetadata(path) if err != nil { @@ -2624,8 +2628,8 @@ func (s *MediaScanner) updateMediaItem(ctx context.Context, mediaItemID pgtype.U if metadata.AlternateInfo != "" { alternateInfoBytes = []byte(metadata.AlternateInfo) } - _, err = s.db.UpdateMediaItem(ctx, database.UpdateMediaItemParams{ - ID: mediaItemID, + params := database.UpdateMediaItemParams{ + ID: existing.ID, Title: metadata.Title, Author: pgtype.Text{String: metadata.Author, Valid: metadata.Author != ""}, Isbn: pgtype.Text{String: utils.NormalizeISBNSafe(metadata.ISBN), Valid: metadata.ISBN != ""}, @@ -2657,19 +2661,34 @@ func (s *MediaScanner) updateMediaItem(ctx context.Context, mediaItemID pgtype.U AlternateInfo: alternateInfoBytes, ScanInformation: pgtype.Text{String: metadata.ScanInformation, Valid: metadata.ScanInformation != ""}, Summary: pgtype.Text{String: metadata.Summary, Valid: metadata.Summary != ""}, - }) + } + + // Keep user-customized fields, and keep the override set itself intact. + utils.ApplyMetadataOverrides(¶ms, existing) + params.MetadataOverrides = existing.MetadataOverrides + + _, err = s.db.UpdateMediaItem(ctx, params) return err } // RescanMediaItem re-extracts metadata for a single media item and updates it. // It is the per-book rescan used by the Edit Metadata dialog and backfills // covers for items imported before the PDF render fallback existed. -func (s *MediaScanner) RescanMediaItem(ctx context.Context, mediaItemID pgtype.UUID) error { +// With resetOverrides, user customizations are discarded first: the item is +// returned to pure scanned defaults (the "Reset to Scanned" action). +func (s *MediaScanner) RescanMediaItem(ctx context.Context, mediaItemID pgtype.UUID, resetOverrides bool) error { item, err := s.db.GetMediaItem(ctx, mediaItemID) if err != nil { return fmt.Errorf("media item not found: %w", err) } + if resetOverrides { + if err := s.db.ClearMediaItemMetadataOverrides(ctx, mediaItemID); err != nil { + return fmt.Errorf("failed to clear metadata overrides: %w", err) + } + item.MetadataOverrides = nil + } + folders, err := s.db.GetLibraryFolders(ctx, item.LibraryID) if err != nil || len(folders) == 0 { return fmt.Errorf("no library folders found for library") @@ -2693,12 +2712,11 @@ func (s *MediaScanner) RescanMediaItem(ctx context.Context, mediaItemID pgtype.U return fmt.Errorf("media file not found on disk: %s", item.FilePath) } - info, err := os.Stat(fullPath) - if err != nil { + if _, err := os.Stat(fullPath); err != nil { return fmt.Errorf("failed to stat media file: %w", err) } - if err := s.updateMediaItem(ctx, mediaItemID, fullPath, info); err != nil { + if err := s.updateMediaItem(ctx, item, fullPath); err != nil { return fmt.Errorf("failed to update media item: %w", err) } s.recomputeHashInfo(ctx, mediaItemID, item.LibraryID, fullPath) diff --git a/internal/utils/metadata_overrides.go b/internal/utils/metadata_overrides.go new file mode 100644 index 0000000..aa0f6f1 --- /dev/null +++ b/internal/utils/metadata_overrides.go @@ -0,0 +1,305 @@ +package utils + +import ( + "strconv" + "strings" + + "bookhoard/internal/database" + + "github.com/jackc/pgx/v5/pgtype" +) + +// Per-field user-override tracking for media item metadata. +// +// metadata_overrides is a TEXT[] column on media_items holding the names of +// columns the user has customized via the metadata editor. Library scans and +// per-book rescans MUST preserve those columns (applyMetadataOverrides); +// only the reset-to-scanned-defaults action clears the set. + +const ( + OverrideTitle = "title" + OverrideAuthor = "author" + OverrideISBN = "isbn" + OverrideDescription = "description" + OverrideCoverImagePath = "cover_image_path" + OverrideSeries = "series" + OverrideSeriesNumber = "series_number" + OverrideTags = "tags" + OverrideAsin = "asin" + OverrideDatePublished = "date_published" + OverridePublisher = "publisher" + OverrideContributors = "contributors" + OverrideLanguage = "language" + OverrideEdition = "edition" + OverridePageCount = "page_count" + OverrideGenre = "genre" + OverrideCopyrightYear = "copyright_year" + OverrideGoodreadsID = "goodreads_id" + OverrideOpenlibraryID = "openlibrary_id" + OverrideGoogleBooksID = "google_books_id" + OverrideMangaType = "manga_type" + OverrideReadingDirection = "reading_direction" + OverrideSeriesCount = "series_count" + OverrideVolume = "volume" + OverrideImprint = "imprint" + OverrideAgeRating = "age_rating" + OverrideWebURL = "web_url" + OverrideMetadataNotes = "metadata_notes" + OverrideCommunityRating = "community_rating" + OverrideStoryArc = "story_arc" + OverrideIsBlackAndWhite = "is_black_and_white" + OverrideAlternateInfo = "alternate_info" + OverrideScanInformation = "scan_information" + OverrideSummary = "summary" +) + +// hasOverride reports whether key is in the override set. +func hasOverride(overrides []string, key string) bool { + for _, k := range overrides { + if k == key { + return true + } + } + return false +} + +// MergeOverrides unions existing and added, preserving order and dropping +// duplicates. Returns a non-nil slice so it always satisfies NOT NULL columns. +func MergeOverrides(existing []string, added ...string) []string { + seen := make(map[string]bool, len(existing)+len(added)) + merged := make([]string, 0, len(existing)+len(added)) + for _, k := range existing { + if k != "" && !seen[k] { + seen[k] = true + merged = append(merged, k) + } + } + for _, k := range added { + if k != "" && !seen[k] { + seen[k] = true + merged = append(merged, k) + } + } + return merged +} + +// Normalizers turn nullable column values into comparable strings so that +// "unset" (invalid/zero) forms compare equal regardless of which side they +// come from. + +func normText(t pgtype.Text) string { + if !t.Valid { + return "" + } + return t.String +} + +func normInt(i pgtype.Int4) string { + if !i.Valid { + return "" + } + return strconv.FormatInt(int64(i.Int32), 10) +} + +func normFloat(f pgtype.Float8) string { + if !f.Valid { + return "" + } + return strconv.FormatFloat(f.Float64, 'g', -1, 64) +} + +func normBool(b pgtype.Bool) string { + if !b.Valid { + return "" + } + return strconv.FormatBool(b.Bool) +} + +func normDate(d pgtype.Date) string { + if !d.Valid { + return "" + } + return d.Time.Format("2006-01-02") +} + +func normStringSlice(s []string) string { + if len(s) == 0 { + return "" + } + return strings.Join(s, "\x1f") +} + +func normBytes(b []byte) string { + if len(b) == 0 { + return "" + } + return string(b) +} + +// detectMetadataOverridesDiff returns the keys whose value in params differs +// from the existing row. Used by the metadata editor save path to grow the +// override set with exactly the fields the user changed. +func detectMetadataOverridesDiff(params database.UpdateMediaItemParams, existing database.MediaItems) []string { + var changed []string + add := func(key string, differs bool) { + if differs { + changed = append(changed, key) + } + } + + add(OverrideTitle, params.Title != existing.Title) + add(OverrideAuthor, normText(params.Author) != normText(existing.Author)) + add(OverrideISBN, normText(params.Isbn) != normText(existing.Isbn)) + add(OverrideDescription, normText(params.Description) != normText(existing.Description)) + add(OverrideSeries, normText(params.Series) != normText(existing.Series)) + add(OverrideSeriesNumber, normInt(params.SeriesNumber) != normInt(existing.SeriesNumber)) + add(OverrideTags, normStringSlice(params.Tags) != normStringSlice(existing.Tags)) + add(OverrideAsin, normText(params.Asin) != normText(existing.Asin)) + add(OverrideDatePublished, normDate(params.DatePublished) != normDate(existing.DatePublished)) + add(OverridePublisher, normText(params.Publisher) != normText(existing.Publisher)) + add(OverrideContributors, normStringSlice(params.Contributors) != normStringSlice(existing.Contributors)) + add(OverrideLanguage, normText(params.Language) != normText(existing.Language)) + add(OverrideEdition, normText(params.Edition) != normText(existing.Edition)) + add(OverridePageCount, normInt(params.PageCount) != normInt(existing.PageCount)) + add(OverrideGenre, normText(params.Genre) != normText(existing.Genre)) + add(OverrideCopyrightYear, normInt(params.CopyrightYear) != normInt(existing.CopyrightYear)) + add(OverrideGoodreadsID, normText(params.GoodreadsID) != normText(existing.GoodreadsID)) + add(OverrideOpenlibraryID, normText(params.OpenlibraryID) != normText(existing.OpenlibraryID)) + add(OverrideGoogleBooksID, normText(params.GoogleBooksID) != normText(existing.GoogleBooksID)) + add(OverrideMangaType, normText(params.MangaType) != normText(existing.MangaType)) + add(OverrideReadingDirection, normText(params.ReadingDirection) != normText(existing.ReadingDirection)) + add(OverrideSeriesCount, normInt(params.SeriesCount) != normInt(existing.SeriesCount)) + add(OverrideVolume, normInt(params.Volume) != normInt(existing.Volume)) + add(OverrideImprint, normText(params.Imprint) != normText(existing.Imprint)) + add(OverrideAgeRating, normText(params.AgeRating) != normText(existing.AgeRating)) + add(OverrideWebURL, normText(params.WebUrl) != normText(existing.WebUrl)) + add(OverrideMetadataNotes, normText(params.MetadataNotes) != normText(existing.MetadataNotes)) + add(OverrideCommunityRating, normFloat(params.CommunityRating) != normFloat(existing.CommunityRating)) + add(OverrideStoryArc, normText(params.StoryArc) != normText(existing.StoryArc)) + add(OverrideIsBlackAndWhite, normBool(params.IsBlackAndWhite) != normBool(existing.IsBlackAndWhite)) + add(OverrideAlternateInfo, normBytes(params.AlternateInfo) != normBytes(existing.AlternateInfo)) + add(OverrideScanInformation, normText(params.ScanInformation) != normText(existing.ScanInformation)) + add(OverrideSummary, normText(params.Summary) != normText(existing.Summary)) + + return changed +} + +// DetectMetadataOverrides returns the union of the existing override set and +// any fields whose incoming (user-submitted) values differ from the stored +// row. Overrides accumulate: a field stays protected until an explicit reset, +// even if a later save reverts the value. +func DetectMetadataOverrides(params database.UpdateMediaItemParams, existing database.MediaItems) []string { + return MergeOverrides(existing.MetadataOverrides, detectMetadataOverridesDiff(params, existing)...) +} + +// ApplyMetadataOverrides restores every overridden field in params from the +// existing row so scanner updates cannot clobber user customizations. Derived +// search columns are restored together with their base column. +func ApplyMetadataOverrides(params *database.UpdateMediaItemParams, existing database.MediaItems) { + o := existing.MetadataOverrides + + if hasOverride(o, OverrideTitle) { + params.Title = existing.Title + } + if hasOverride(o, OverrideAuthor) { + params.Author = existing.Author + } + if hasOverride(o, OverrideISBN) { + params.Isbn = existing.Isbn + } + if hasOverride(o, OverrideDescription) { + params.Description = existing.Description + } + if hasOverride(o, OverrideCoverImagePath) { + params.CoverImagePath = existing.CoverImagePath + } + if hasOverride(o, OverrideSeries) { + params.Series = existing.Series + } + if hasOverride(o, OverrideSeriesNumber) { + params.SeriesNumber = existing.SeriesNumber + } + if hasOverride(o, OverrideTags) { + params.Tags = existing.Tags + params.TagsSearch = existing.TagsSearch + } + if hasOverride(o, OverrideAsin) { + params.Asin = existing.Asin + } + if hasOverride(o, OverrideDatePublished) { + params.DatePublished = existing.DatePublished + } + if hasOverride(o, OverridePublisher) { + params.Publisher = existing.Publisher + } + if hasOverride(o, OverrideContributors) { + params.Contributors = existing.Contributors + params.ContributorsSearch = existing.ContributorsSearch + } + if hasOverride(o, OverrideLanguage) { + params.Language = existing.Language + } + if hasOverride(o, OverrideEdition) { + params.Edition = existing.Edition + } + if hasOverride(o, OverridePageCount) { + params.PageCount = existing.PageCount + } + if hasOverride(o, OverrideGenre) { + params.Genre = existing.Genre + } + if hasOverride(o, OverrideCopyrightYear) { + params.CopyrightYear = existing.CopyrightYear + } + if hasOverride(o, OverrideGoodreadsID) { + params.GoodreadsID = existing.GoodreadsID + } + if hasOverride(o, OverrideOpenlibraryID) { + params.OpenlibraryID = existing.OpenlibraryID + } + if hasOverride(o, OverrideGoogleBooksID) { + params.GoogleBooksID = existing.GoogleBooksID + } + if hasOverride(o, OverrideMangaType) { + params.MangaType = existing.MangaType + } + if hasOverride(o, OverrideReadingDirection) { + params.ReadingDirection = existing.ReadingDirection + } + if hasOverride(o, OverrideSeriesCount) { + params.SeriesCount = existing.SeriesCount + } + if hasOverride(o, OverrideVolume) { + params.Volume = existing.Volume + } + if hasOverride(o, OverrideImprint) { + params.Imprint = existing.Imprint + } + if hasOverride(o, OverrideAgeRating) { + params.AgeRating = existing.AgeRating + } + if hasOverride(o, OverrideWebURL) { + params.WebUrl = existing.WebUrl + } + if hasOverride(o, OverrideMetadataNotes) { + params.MetadataNotes = existing.MetadataNotes + } + if hasOverride(o, OverrideCommunityRating) { + params.CommunityRating = existing.CommunityRating + } + if hasOverride(o, OverrideStoryArc) { + params.StoryArc = existing.StoryArc + } + if hasOverride(o, OverrideIsBlackAndWhite) { + params.IsBlackAndWhite = existing.IsBlackAndWhite + } + if hasOverride(o, OverrideAlternateInfo) { + params.AlternateInfo = existing.AlternateInfo + } + if hasOverride(o, OverrideScanInformation) { + params.ScanInformation = existing.ScanInformation + } + if hasOverride(o, OverrideSummary) { + params.Summary = existing.Summary + } +} diff --git a/internal/utils/metadata_overrides_test.go b/internal/utils/metadata_overrides_test.go new file mode 100644 index 0000000..95c8145 --- /dev/null +++ b/internal/utils/metadata_overrides_test.go @@ -0,0 +1,112 @@ +package utils + +import ( + "reflect" + "testing" + + "bookhoard/internal/database" + + "github.com/jackc/pgx/v5/pgtype" +) + +func TestMergeOverrides(t *testing.T) { + got := MergeOverrides([]string{"title", "tags"}, "tags", "description", "") + want := []string{"title", "tags", "description"} + if !reflect.DeepEqual(got, want) { + t.Errorf("MergeOverrides() = %v, want %v", got, want) + } + if MergeOverrides(nil) == nil { + t.Error("MergeOverrides(nil) must return non-nil slice") + } +} + +func TestDetectMetadataOverrides(t *testing.T) { + existing := database.MediaItems{ + Title: "Scanned Title", + Description: pgtype.Text{String: "Scanned description", Valid: true}, + Tags: []string{"foo", "bar"}, + } + params := database.UpdateMediaItemParams{ + // unchanged + Title: "Scanned Title", + // changed + Description: pgtype.Text{String: "My custom description", Valid: true}, + Tags: []string{"foo", "bar"}, + } + + got := DetectMetadataOverrides(params, existing) + if !reflect.DeepEqual(got, []string{"description"}) { + t.Errorf("DetectMetadataOverrides() = %v, want [description]", got) + } + + // Overrides accumulate: an existing override survives a later save. + existing.MetadataOverrides = []string{"publisher"} + got = DetectMetadataOverrides(params, existing) + want := []string{"publisher", "description"} + if !reflect.DeepEqual(got, want) { + t.Errorf("DetectMetadataOverrides() accumulate = %v, want %v", got, want) + } + + // An untouched save detects nothing new. + noop := database.UpdateMediaItemParams{ + Title: existing.Title, + Description: existing.Description, + Tags: existing.Tags, + } + got = DetectMetadataOverrides(noop, existing) + if !reflect.DeepEqual(got, []string{"publisher"}) { + t.Errorf("DetectMetadataOverrides() noop = %v, want [publisher]", got) + } +} + +func TestDetectMetadataOverridesUnsetFormsEqual(t *testing.T) { + // Zero-value params must not look "changed" against NULL-ish columns. + existing := database.MediaItems{ + PageCount: pgtype.Int4{Int32: 0, Valid: false}, + SeriesNumber: pgtype.Int4{Int32: 5, Valid: true}, + CommunityRating: pgtype.Float8{Float64: 0, Valid: false}, + } + params := database.UpdateMediaItemParams{ + PageCount: pgtype.Int4{Int32: 0, Valid: false}, + SeriesNumber: pgtype.Int4{Int32: 0, Valid: false}, // cleared by user -> changed + CommunityRating: pgtype.Float8{Float64: 0, Valid: false}, + } + got := DetectMetadataOverrides(params, existing) + if !reflect.DeepEqual(got, []string{"series_number"}) { + t.Errorf("DetectMetadataOverrides() = %v, want [series_number]", got) + } +} + +func TestApplyMetadataOverrides(t *testing.T) { + existing := database.MediaItems{ + Title: "Custom Title", + Description: pgtype.Text{String: "Custom desc", Valid: true}, + Tags: []string{"mine"}, + TagsSearch: []string{"mine"}, + CoverImagePath: pgtype.Text{String: "books/x.epub.custom_cover.jpg", Valid: true}, + } + params := database.UpdateMediaItemParams{ + Title: "Scanned Title", + Description: pgtype.Text{String: "Scanned desc", Valid: true}, + Tags: []string{"scanned"}, + TagsSearch: []string{"scanned"}, + CoverImagePath: pgtype.Text{String: "books/x.epub.cover.jpg", Valid: true}, + } + + // Only title and tags overridden; scanned description and cover win. + existing.MetadataOverrides = []string{"title", "tags"} + ApplyMetadataOverrides(¶ms, existing) + + if params.Title != "Custom Title" { + t.Errorf("Title = %q, want %q", params.Title, "Custom Title") + } + if !reflect.DeepEqual(params.Tags, []string{"mine"}) || !reflect.DeepEqual(params.TagsSearch, []string{"mine"}) { + t.Error("Tags/TagsSearch must be restored together") + } + if params.Description.String != "Scanned desc" { + t.Errorf("Description = %q, want scanned value", params.Description.String) + } + if params.CoverImagePath.String != "books/x.epub.cover.jpg" { + t.Errorf("CoverImagePath = %q, want scanned value", params.CoverImagePath.String) + } +}