feat(metadata): per-field user overrides that survive library rescans

Previously both the metadata editor (PUT /api/media-items/:id) and the
scanner (library scans, force rescans, per-book rescans) wrote through
the same unconditional UPDATE media_items query, so any rescan wiped
user-written descriptions, tags, and uploaded covers. Custom and scanned
values were indistinguishable, and custom cover uploads even wrote to
the same {file}.cover.jpg sidecar path the scanner generates, so each
side silently clobbered the other.

Introduce metadata_overrides, a TEXT[] column on media_items listing the
column names the user has customized:

- Saving metadata records overrides per field by diffing the submitted
  values against the stored row (an untouched save records nothing);
  overrides accumulate until an explicit reset. Cover upload/removal
  always marks cover_image_path. Bulk updates mark each applied field.
- The scanner merges: updateMediaItem() now takes the existing row and
  restores every overridden column (including derived *_search arrays)
  before writing, and preserves the override set itself.
- Uploaded covers move to a dedicated {file}.custom_cover.{jpg|png|webp}
  sidecar so the scanner can never overwrite a user cover on disk.
- RescanMediaItem gains resetOverrides: POST /api/media-items/:id/rescan?
  reset_overrides=true clears the set first, returning the item to pure
  scanned defaults.

Shared detection/restore helpers live in internal/utils
(metadata_overrides.go) with unit tests covering detection, accumulation,
unset-form equality, and restore-with-derived-fields. Schema change is
an idempotent ADD COLUMN IF NOT EXISTS applied on startup. Also includes
incidental gofmt of NewMediaScanner literals in media_scanner.go.
This commit is contained in:
John O'Keefe
2026-09-12 14:21:35 -04:00
parent b9645752f5
commit 7f64b92b9d
10 changed files with 653 additions and 111 deletions
+11
View File
@@ -207,6 +207,12 @@ CREATE TABLE IF NOT EXISTS media_items (
-- Summary (distinct from description - may merge with Calibre description) -- Summary (distinct from description - may merge with Calibre description)
summary TEXT, -- Summary from ComicInfo.xml (may be merged with description from Calibre) 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 -- Chapter metadata for reader navigation and progress tracking
-- Caches detected chapter structure to avoid re-parsing files -- Caches detected chapter structure to avoid re-parsing files
-- Populated by ReaderService.DetectChapters() on first read -- 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 tags_search TEXT[];
ALTER TABLE media_items ADD COLUMN IF NOT EXISTS contributors_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 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_tags_search ON media_items USING GIN (tags_search);
CREATE INDEX IF NOT EXISTS idx_media_items_contributors_search ON media_items USING GIN (contributors_search); CREATE INDEX IF NOT EXISTS idx_media_items_contributors_search ON media_items USING GIN (contributors_search);
+1 -1
View File
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.31.1
package database package database
+2 -1
View File
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.31.1
package database package database
@@ -306,6 +306,7 @@ type MediaItems struct {
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
// Summary from ComicInfo.xml (may be merged with description from Calibre) // Summary from ComicInfo.xml (may be merged with description from Calibre)
Summary pgtype.Text `db:"summary" json:"summary"` Summary pgtype.Text `db:"summary" json:"summary"`
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
TagsSearch []string `db:"tags_search" json:"tags_search"` TagsSearch []string `db:"tags_search" json:"tags_search"`
+4 -1
View File
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.31.1
package database package database
@@ -30,6 +30,9 @@ type Querier interface {
ClearDeviceSyncQueue(ctx context.Context, deviceID pgtype.UUID) error ClearDeviceSyncQueue(ctx context.Context, deviceID pgtype.UUID) error
ClearKoboShelf(ctx context.Context, deviceID pgtype.UUID) error ClearKoboShelf(ctx context.Context, deviceID pgtype.UUID) error
ClearKoboShelfByName(ctx context.Context, arg ClearKoboShelfByNameParams) 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) CountAdmins(ctx context.Context) (int64, error)
// Count unlinked books for a device // Count unlinked books for a device
CountUnlinkedBooks(ctx context.Context, deviceID pgtype.UUID) (int64, error) CountUnlinkedBooks(ctx context.Context, deviceID pgtype.UUID) (int64, error)
+81 -31
View File
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.30.0 // sqlc v1.31.1
// source: queries.sql // source: queries.sql
package database package database
@@ -226,6 +226,18 @@ func (q *Queries) ClearKoboShelfByName(ctx context.Context, arg ClearKoboShelfBy
return err 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 const CountAdmins = `-- name: CountAdmins :one
SELECT COUNT(*) FROM users WHERE role = 'admin' 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) 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) 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() 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 { type CreateMediaItemParams struct {
@@ -1053,6 +1065,7 @@ func (q *Queries) CreateMediaItem(ctx context.Context, arg CreateMediaItemParams
&i.AlternateInfo, &i.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -2447,7 +2460,7 @@ func (q *Queries) GetAnnotationsForBook(ctx context.Context, arg GetAnnotationsF
} }
const GetBooksByTag = `-- name: GetBooksByTag :many 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] WHERE library_id = $1 AND tags @> ARRAY[$2::text]
ORDER BY title ASC ORDER BY title ASC
` `
@@ -2520,6 +2533,7 @@ func (q *Queries) GetBooksByTag(ctx context.Context, arg GetBooksByTagParams) ([
&i.AlternateInfo, &i.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -2619,7 +2633,7 @@ func (q *Queries) GetCollectionItems(ctx context.Context, collectionID pgtype.UU
} }
const GetCollectionItemsForDashboard = `-- name: GetCollectionItemsForDashboard :many 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 INNER JOIN collection_items ci ON ci.media_item_id = mi.id
WHERE ci.collection_id = $1 WHERE ci.collection_id = $1
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid) 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"` AlternateInfo []byte `db:"alternate_info" json:"alternate_info"`
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
Summary pgtype.Text `db:"summary" json:"summary"` Summary pgtype.Text `db:"summary" json:"summary"`
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
TagsSearch []string `db:"tags_search" json:"tags_search"` TagsSearch []string `db:"tags_search" json:"tags_search"`
@@ -2761,6 +2776,7 @@ func (q *Queries) GetCollectionItemsForDashboard(ctx context.Context, arg GetCol
&i.AlternateInfo, &i.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -2913,7 +2929,7 @@ func (q *Queries) GetCollectionsForBook(ctx context.Context, mediaItemID pgtype.
} }
const GetContinueReadingItems = `-- name: GetContinueReadingItems :many 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 ( INNER JOIN (
SELECT DISTINCT ON (media_item_id) media_item_id, last_read_at SELECT DISTINCT ON (media_item_id) media_item_id, last_read_at
FROM reading_progress FROM reading_progress
@@ -2997,6 +3013,7 @@ func (q *Queries) GetContinueReadingItems(ctx context.Context, arg GetContinueRe
&i.AlternateInfo, &i.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -3030,7 +3047,7 @@ WITH user_series_progress AS (
GROUP BY mi.series GROUP BY mi.series
), ),
next_books AS ( 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 usp.last_read_at
FROM media_items mi FROM media_items mi
JOIN user_series_progress usp ON mi.series = usp.series 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) 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 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 ORDER BY last_read_at DESC NULLS LAST
LIMIT $1 LIMIT $1
` `
@@ -3103,6 +3120,7 @@ type GetContinueSeriesItemsRow struct {
AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` AlternateInfo []byte `db:"alternate_info" json:"alternate_info"`
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
Summary pgtype.Text `db:"summary" json:"summary"` Summary pgtype.Text `db:"summary" json:"summary"`
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
TagsSearch []string `db:"tags_search" json:"tags_search"` TagsSearch []string `db:"tags_search" json:"tags_search"`
@@ -3177,6 +3195,7 @@ func (q *Queries) GetContinueSeriesItems(ctx context.Context, arg GetContinueSer
&i.AlternateInfo, &i.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -4306,7 +4325,7 @@ func (q *Queries) GetLibraryFolders(ctx context.Context, libraryID pgtype.UUID)
} }
const GetLibraryItems = `-- name: GetLibraryItems :many 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) WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid)
ORDER BY mi.created_at DESC ORDER BY mi.created_at DESC
` `
@@ -4374,6 +4393,7 @@ func (q *Queries) GetLibraryItems(ctx context.Context, libraryID pgtype.UUID) ([
&i.AlternateInfo, &i.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -4805,7 +4825,7 @@ func (q *Queries) GetMediaHighlights(ctx context.Context, arg GetMediaHighlights
} }
const GetMediaItem = `-- name: GetMediaItem :one 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) { 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.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -4878,7 +4899,7 @@ func (q *Queries) GetMediaItem(ctx context.Context, id pgtype.UUID) (MediaItems,
} }
const GetMediaItemByFilePath = `-- name: GetMediaItemByFilePath :one 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 { type GetMediaItemByFilePathParams struct {
@@ -4943,6 +4964,7 @@ func (q *Queries) GetMediaItemByFilePath(ctx context.Context, arg GetMediaItemBy
&i.AlternateInfo, &i.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -4956,7 +4978,7 @@ func (q *Queries) GetMediaItemByFilePath(ctx context.Context, arg GetMediaItemBy
} }
const GetMediaItemByFilePathAnyLibrary = `-- name: GetMediaItemByFilePathAnyLibrary :one 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) { 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.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -5030,7 +5053,7 @@ func (q *Queries) GetMediaItemByFilePathAnyLibrary(ctx context.Context, filePath
const GetMediaItemByFilePathForSync = `-- name: GetMediaItemByFilePathForSync :one 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.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -5106,7 +5130,7 @@ func (q *Queries) GetMediaItemByFilePathForSync(ctx context.Context, filePath st
} }
const GetMediaItemByKoboContentId = `-- name: GetMediaItemByKoboContentId :one 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) { 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.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -5179,7 +5204,7 @@ func (q *Queries) GetMediaItemByKoboContentId(ctx context.Context, koboContentID
} }
const GetMediaItemByOPFIdentifier = `-- name: GetMediaItemByOPFIdentifier :one 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 // Get media item by OPF identifier
@@ -5240,6 +5265,7 @@ func (q *Queries) GetMediaItemByOPFIdentifier(ctx context.Context, opfIdentifier
&i.AlternateInfo, &i.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -5253,7 +5279,7 @@ func (q *Queries) GetMediaItemByOPFIdentifier(ctx context.Context, opfIdentifier
} }
const GetMediaItemByOPFUUID = `-- name: GetMediaItemByOPFUUID :one 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 // Get media item by OPF UUID
@@ -5314,6 +5340,7 @@ func (q *Queries) GetMediaItemByOPFUUID(ctx context.Context, opfUuid pgtype.Text
&i.AlternateInfo, &i.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -5327,7 +5354,7 @@ func (q *Queries) GetMediaItemByOPFUUID(ctx context.Context, opfUuid pgtype.Text
} }
const GetMediaItemBySHA256 = `-- name: GetMediaItemBySHA256 :one 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 // Get media item by SHA-256 hash
@@ -5388,6 +5415,7 @@ func (q *Queries) GetMediaItemBySHA256(ctx context.Context, fileSha256 pgtype.Te
&i.AlternateInfo, &i.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -5401,7 +5429,7 @@ func (q *Queries) GetMediaItemBySHA256(ctx context.Context, fileSha256 pgtype.Te
} }
const GetMediaItemBySHA256AndLibrary = `-- name: GetMediaItemBySHA256AndLibrary :one 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 { type GetMediaItemBySHA256AndLibraryParams struct {
@@ -5467,6 +5495,7 @@ func (q *Queries) GetMediaItemBySHA256AndLibrary(ctx context.Context, arg GetMed
&i.AlternateInfo, &i.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -5807,7 +5836,7 @@ func (q *Queries) GetNextRetryTime(ctx context.Context) (interface{}, error) {
} }
const GetNotStartedItems = `-- name: GetNotStartedItems :many 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) WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid)
AND NOT EXISTS ( AND NOT EXISTS (
SELECT 1 FROM reading_progress rp SELECT 1 FROM reading_progress rp
@@ -5888,6 +5917,7 @@ func (q *Queries) GetNotStartedItems(ctx context.Context, arg GetNotStartedItems
&i.AlternateInfo, &i.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -6207,7 +6237,7 @@ func (q *Queries) GetReadingSpeed(ctx context.Context, arg GetReadingSpeedParams
} }
const GetRecentlyAddedItems = `-- name: GetRecentlyAddedItems :many 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) WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid)
ORDER BY mi.imported_at DESC NULLS LAST, mi.created_at DESC ORDER BY mi.imported_at DESC NULLS LAST, mi.created_at DESC
LIMIT $2 LIMIT $2
@@ -6281,6 +6311,7 @@ func (q *Queries) GetRecentlyAddedItems(ctx context.Context, arg GetRecentlyAdde
&i.AlternateInfo, &i.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -6301,7 +6332,7 @@ func (q *Queries) GetRecentlyAddedItems(ctx context.Context, arg GetRecentlyAdde
} }
const GetRecentlyReadItems = `-- name: GetRecentlyReadItems :many 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 ( INNER JOIN (
SELECT DISTINCT ON (media_item_id) media_item_id, last_read_at SELECT DISTINCT ON (media_item_id) media_item_id, last_read_at
FROM reading_progress FROM reading_progress
@@ -6383,6 +6414,7 @@ func (q *Queries) GetRecentlyReadItems(ctx context.Context, arg GetRecentlyReadI
&i.AlternateInfo, &i.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -6503,7 +6535,7 @@ func (q *Queries) GetSavedFilters(ctx context.Context, arg GetSavedFiltersParams
} }
const GetSeriesBooks = `-- name: GetSeriesBooks :many 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 WHERE series = $1
ORDER BY series_number ASC NULLS LAST 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.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -8354,7 +8387,7 @@ func (q *Queries) ListLibraries(ctx context.Context) ([]ListLibrariesRow, error)
} }
const ListMediaItems = `-- name: ListMediaItems :many 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 FROM media_items mi
JOIN libraries l ON mi.library_id = l.id JOIN libraries l ON mi.library_id = l.id
JOIN library_types lt ON l.library_type_id = lt.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"` AlternateInfo []byte `db:"alternate_info" json:"alternate_info"`
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
Summary pgtype.Text `db:"summary" json:"summary"` Summary pgtype.Text `db:"summary" json:"summary"`
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
TagsSearch []string `db:"tags_search" json:"tags_search"` TagsSearch []string `db:"tags_search" json:"tags_search"`
@@ -8495,6 +8529,7 @@ func (q *Queries) ListMediaItems(ctx context.Context, arg ListMediaItemsParams)
&i.AlternateInfo, &i.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -8517,7 +8552,7 @@ func (q *Queries) ListMediaItems(ctx context.Context, arg ListMediaItemsParams)
} }
const ListMediaItemsByLibrary = `-- name: ListMediaItemsByLibrary :many 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 FROM media_items mi
JOIN libraries l ON mi.library_id = l.id JOIN libraries l ON mi.library_id = l.id
JOIN library_types lt ON l.library_type_id = lt.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"` AlternateInfo []byte `db:"alternate_info" json:"alternate_info"`
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
Summary pgtype.Text `db:"summary" json:"summary"` Summary pgtype.Text `db:"summary" json:"summary"`
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
TagsSearch []string `db:"tags_search" json:"tags_search"` TagsSearch []string `db:"tags_search" json:"tags_search"`
@@ -8654,6 +8690,7 @@ func (q *Queries) ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype.
&i.AlternateInfo, &i.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -8676,7 +8713,7 @@ func (q *Queries) ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype.
} }
const ListMediaItemsBySHA256AndLibrary = `-- name: ListMediaItemsBySHA256AndLibrary :many 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 { type ListMediaItemsBySHA256AndLibraryParams struct {
@@ -8748,6 +8785,7 @@ func (q *Queries) ListMediaItemsBySHA256AndLibrary(ctx context.Context, arg List
&i.AlternateInfo, &i.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -8768,7 +8806,7 @@ func (q *Queries) ListMediaItemsBySHA256AndLibrary(ctx context.Context, arg List
} }
const ListMediaItemsMissingHash = `-- name: ListMediaItemsMissingHash :many 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) // 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.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -8855,7 +8894,7 @@ func (q *Queries) ListMediaItemsMissingHash(ctx context.Context) ([]MediaItems,
} }
const ListMediaItemsSorted = `-- name: ListMediaItemsSorted :many 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 FROM media_items mi
JOIN libraries l ON mi.library_id = l.id JOIN libraries l ON mi.library_id = l.id
JOIN library_types lt ON l.library_type_id = lt.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"` AlternateInfo []byte `db:"alternate_info" json:"alternate_info"`
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
Summary pgtype.Text `db:"summary" json:"summary"` Summary pgtype.Text `db:"summary" json:"summary"`
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
TagsSearch []string `db:"tags_search" json:"tags_search"` TagsSearch []string `db:"tags_search" json:"tags_search"`
@@ -9070,6 +9110,7 @@ func (q *Queries) ListMediaItemsSorted(ctx context.Context, arg ListMediaItemsSo
&i.AlternateInfo, &i.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -10193,7 +10234,7 @@ func (q *Queries) SearchLanguageValues(ctx context.Context, arg SearchLanguageVa
} }
const SearchMediaItems = `-- name: SearchMediaItems :many 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 FROM media_items mi
JOIN libraries l ON mi.library_id = l.id JOIN libraries l ON mi.library_id = l.id
JOIN library_types lt ON l.library_type_id = lt.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"` AlternateInfo []byte `db:"alternate_info" json:"alternate_info"`
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
Summary pgtype.Text `db:"summary" json:"summary"` Summary pgtype.Text `db:"summary" json:"summary"`
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
TagsSearch []string `db:"tags_search" json:"tags_search"` TagsSearch []string `db:"tags_search" json:"tags_search"`
@@ -10363,6 +10405,7 @@ func (q *Queries) SearchMediaItems(ctx context.Context, arg SearchMediaItemsPara
&i.AlternateInfo, &i.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -11834,9 +11877,10 @@ UPDATE media_items SET
alternate_info = $35, alternate_info = $35,
scan_information = $36, scan_information = $36,
summary = $37, summary = $37,
metadata_overrides = $38,
updated_at = NOW() updated_at = NOW()
WHERE id = $1 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 { type UpdateMediaItemParams struct {
@@ -11877,6 +11921,7 @@ type UpdateMediaItemParams struct {
AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` AlternateInfo []byte `db:"alternate_info" json:"alternate_info"`
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
Summary pgtype.Text `db:"summary" json:"summary"` 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) { 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.AlternateInfo,
arg.ScanInformation, arg.ScanInformation,
arg.Summary, arg.Summary,
arg.MetadataOverrides,
) )
var i MediaItems var i MediaItems
err := row.Scan( err := row.Scan(
@@ -11974,6 +12020,7 @@ func (q *Queries) UpdateMediaItem(ctx context.Context, arg UpdateMediaItemParams
&i.AlternateInfo, &i.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -11990,7 +12037,7 @@ const UpdateMediaItemChapterMetadata = `-- name: UpdateMediaItemChapterMetadata
UPDATE media_items UPDATE media_items
SET chapter_metadata = $2, updated_at = NOW() SET chapter_metadata = $2, updated_at = NOW()
WHERE id = $1 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 { type UpdateMediaItemChapterMetadataParams struct {
@@ -12055,6 +12102,7 @@ func (q *Queries) UpdateMediaItemChapterMetadata(ctx context.Context, arg Update
&i.AlternateInfo, &i.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -12162,7 +12210,7 @@ SET
hash_confidence = $5, hash_confidence = $5,
updated_at = NOW() updated_at = NOW()
WHERE id = $1 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 { type UpdateMediaItemIdentifiersParams struct {
@@ -12241,6 +12289,7 @@ func (q *Queries) UpdateMediaItemIdentifiers(ctx context.Context, arg UpdateMedi
&i.AlternateInfo, &i.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
@@ -12261,7 +12310,7 @@ SET entitlement_id = $2,
kobo_metadata = $5, kobo_metadata = $5,
updated_at = NOW() updated_at = NOW()
WHERE id = $1 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 { type UpdateMediaItemKoboMetadataParams struct {
@@ -12335,6 +12384,7 @@ func (q *Queries) UpdateMediaItemKoboMetadata(ctx context.Context, arg UpdateMed
&i.AlternateInfo, &i.AlternateInfo,
&i.ScanInformation, &i.ScanInformation,
&i.Summary, &i.Summary,
&i.MetadataOverrides,
&i.ChapterMetadata, &i.ChapterMetadata,
&i.LibraryTypeName, &i.LibraryTypeName,
&i.TagsSearch, &i.TagsSearch,
+7
View File
@@ -282,10 +282,17 @@ UPDATE media_items SET
alternate_info = $35, alternate_info = $35,
scan_information = $36, scan_information = $36,
summary = $37, summary = $37,
metadata_overrides = $38,
updated_at = NOW() updated_at = NOW()
WHERE id = $1 WHERE id = $1
RETURNING *; 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 -- name: DeleteMediaItem :exec
DELETE FROM media_items WHERE id = $1; DELETE FROM media_items WHERE id = $1;
+90 -55
View File
@@ -63,41 +63,41 @@ type CreateMediaItemRequest struct {
// UpdateMediaItemRequest represents the request for updating a media item // UpdateMediaItemRequest represents the request for updating a media item
type UpdateMediaItemRequest struct { type UpdateMediaItemRequest struct {
Title string `form:"title" json:"title" validate:"required,min=1,max=500"` Title string `form:"title" json:"title" validate:"required,min=1,max=500"`
Author string `form:"author" json:"author"` Author string `form:"author" json:"author"`
ISBN string `form:"isbn" json:"isbn"` ISBN string `form:"isbn" json:"isbn"`
Description string `form:"description" json:"description"` Description string `form:"description" json:"description"`
CoverImagePath string `form:"cover_image_path" json:"cover_image_path"` CoverImagePath string `form:"cover_image_path" json:"cover_image_path"`
CoverAction string `form:"cover_action" json:"cover_action"` CoverAction string `form:"cover_action" json:"cover_action"`
Series string `form:"series" json:"series"` Series string `form:"series" json:"series"`
SeriesNumber int32 `form:"series_number" json:"series_number"` SeriesNumber int32 `form:"series_number" json:"series_number"`
Tags []string `form:"tags" json:"tags"` Tags []string `form:"tags" json:"tags"`
ASIN string `form:"asin" json:"asin"` ASIN string `form:"asin" json:"asin"`
DatePublished string `form:"date_published" json:"date_published"` DatePublished string `form:"date_published" json:"date_published"`
Publisher string `form:"publisher" json:"publisher"` Publisher string `form:"publisher" json:"publisher"`
Contributors []string `form:"contributors" json:"contributors"` Contributors []string `form:"contributors" json:"contributors"`
Language string `form:"language" json:"language"` Language string `form:"language" json:"language"`
Edition string `form:"edition" json:"edition"` Edition string `form:"edition" json:"edition"`
PageCount int32 `form:"page_count" json:"page_count"` PageCount int32 `form:"page_count" json:"page_count"`
Genre string `form:"genre" json:"genre"` Genre string `form:"genre" json:"genre"`
CopyrightYear int32 `form:"copyright_year" json:"copyright_year"` CopyrightYear int32 `form:"copyright_year" json:"copyright_year"`
GoodreadsID string `form:"goodreads_id" json:"goodreads_id"` GoodreadsID string `form:"goodreads_id" json:"goodreads_id"`
OpenlibraryID string `form:"openlibrary_id" json:"openlibrary_id"` OpenlibraryID string `form:"openlibrary_id" json:"openlibrary_id"`
GoogleBooksID string `form:"google_books_id" json:"google_books_id"` GoogleBooksID string `form:"google_books_id" json:"google_books_id"`
MangaType string `form:"manga_type" json:"manga_type"` MangaType string `form:"manga_type" json:"manga_type"`
ReadingDirection string `form:"reading_direction" json:"reading_direction"` ReadingDirection string `form:"reading_direction" json:"reading_direction"`
SeriesCount int32 `form:"series_count" json:"series_count"` SeriesCount int32 `form:"series_count" json:"series_count"`
Volume int32 `form:"volume" json:"volume"` Volume int32 `form:"volume" json:"volume"`
Imprint string `form:"imprint" json:"imprint"` Imprint string `form:"imprint" json:"imprint"`
AgeRating string `form:"age_rating" json:"age_rating"` AgeRating string `form:"age_rating" json:"age_rating"`
WebURL string `form:"web_url" json:"web_url"` WebURL string `form:"web_url" json:"web_url"`
MetadataNotes string `form:"metadata_notes" json:"metadata_notes"` MetadataNotes string `form:"metadata_notes" json:"metadata_notes"`
CommunityRating float64 `form:"community_rating" json:"community_rating"` CommunityRating float64 `form:"community_rating" json:"community_rating"`
StoryArc string `form:"story_arc" json:"story_arc"` StoryArc string `form:"story_arc" json:"story_arc"`
IsBlackAndWhite bool `form:"is_black_and_white" json:"is_black_and_white"` IsBlackAndWhite bool `form:"is_black_and_white" json:"is_black_and_white"`
AlternateInfo string `form:"alternate_info" json:"alternate_info"` AlternateInfo string `form:"alternate_info" json:"alternate_info"`
ScanInformation string `form:"scan_information" json:"scan_information"` ScanInformation string `form:"scan_information" json:"scan_information"`
Summary string `form:"summary" json:"summary"` Summary string `form:"summary" json:"summary"`
} }
// CreateMediaNoteRequest represents the request for creating a media note // CreateMediaNoteRequest represents the request for creating a media note
@@ -572,24 +572,33 @@ func (h *MediaHandler) HandleBulkUpdate(c *echo.Context) error {
Summary: existingMedia.Summary, 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 { if update.Updates.Title != nil {
updateParams.Title = *update.Updates.Title updateParams.Title = *update.Updates.Title
overrides = utils.MergeOverrides(overrides, utils.OverrideTitle)
} }
if update.Updates.Author != nil { if update.Updates.Author != nil {
updateParams.Author = pgtype.Text{String: *update.Updates.Author, Valid: true} updateParams.Author = pgtype.Text{String: *update.Updates.Author, Valid: true}
overrides = utils.MergeOverrides(overrides, utils.OverrideAuthor)
} }
if update.Updates.Genre != nil { if update.Updates.Genre != nil {
updateParams.Genre = pgtype.Text{String: *update.Updates.Genre, Valid: true} updateParams.Genre = pgtype.Text{String: *update.Updates.Genre, Valid: true}
overrides = utils.MergeOverrides(overrides, utils.OverrideGenre)
} }
if update.Updates.Language != nil { if update.Updates.Language != nil {
updateParams.Language = pgtype.Text{String: *update.Updates.Language, Valid: true} updateParams.Language = pgtype.Text{String: *update.Updates.Language, Valid: true}
overrides = utils.MergeOverrides(overrides, utils.OverrideLanguage)
} }
if len(update.Updates.Tags) > 0 { if len(update.Updates.Tags) > 0 {
normalizedTags := utils.NormalizeTags(update.Updates.Tags) normalizedTags := utils.NormalizeTags(update.Updates.Tags)
updateParams.Tags = normalizedTags updateParams.Tags = normalizedTags
tagsSearch := utils.NormalizeTagsSearch(update.Updates.Tags) tagsSearch := utils.NormalizeTagsSearch(update.Updates.Tags)
updateParams.TagsSearch = tagsSearch updateParams.TagsSearch = tagsSearch
overrides = utils.MergeOverrides(overrides, utils.OverrideTags)
} }
updateParams.MetadataOverrides = overrides
_, err = h.db.UpdateMediaItem(c.Request().Context(), updateParams) _, err = h.db.UpdateMediaItem(c.Request().Context(), updateParams)
if err != nil { if err != nil {
@@ -925,22 +934,22 @@ func (mh *MediaHandler) GetMediaReadingProgress(c *echo.Context) error {
} }
resp := map[string]interface{}{ resp := map[string]interface{}{
"id": progress.ID, "id": progress.ID,
"media_item_id": progress.MediaItemID, "media_item_id": progress.MediaItemID,
"user_id": progress.UserID, "user_id": progress.UserID,
"current_page": progress.CurrentPage, "current_page": progress.CurrentPage,
"total_pages": progress.TotalPages, "total_pages": progress.TotalPages,
"last_read_at": progress.LastReadAt, "last_read_at": progress.LastReadAt,
"percentage": progress.Percentage, "percentage": progress.Percentage,
"character_offset": progress.CharacterOffset, "character_offset": progress.CharacterOffset,
"epubcfi": progress.Epubcfi, "epubcfi": progress.Epubcfi,
"chapter": progress.Chapter, "chapter": progress.Chapter,
"chapter_progress": progress.ChapterProgress, "chapter_progress": progress.ChapterProgress,
"format_group": progress.FormatGroup, "format_group": progress.FormatGroup,
"total_characters": progress.TotalCharacters, "total_characters": progress.TotalCharacters,
"chapter_count": progress.ChapterCount, "chapter_count": progress.ChapterCount,
"last_sync_device": progress.LastSyncDevice, "last_sync_device": progress.LastSyncDevice,
"last_sync_source": progress.LastSyncSource, "last_sync_source": progress.LastSyncSource,
"last_sync_timestamp": progress.LastSyncTimestamp, "last_sync_timestamp": progress.LastSyncTimestamp,
} }
@@ -1240,6 +1249,7 @@ func (mh *MediaHandler) UpdateMediaItem(c *echo.Context) error {
} }
coverPath := existing.CoverImagePath.String coverPath := existing.CoverImagePath.String
coverUploaded := false
if req.CoverAction == "remove" { if req.CoverAction == "remove" {
coverPath = "" 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"}) return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to save cover image"})
} }
coverPath = savedPath coverPath = savedPath
coverUploaded = true
} }
} }
@@ -1259,7 +1270,7 @@ func (mh *MediaHandler) UpdateMediaItem(c *echo.Context) error {
alternateInfoBytes = []byte(req.AlternateInfo) alternateInfoBytes = []byte(req.AlternateInfo)
} }
item, err := mh.db.UpdateMediaItem(c.Request().Context(), database.UpdateMediaItemParams{ params := database.UpdateMediaItemParams{
ID: pgtype.UUID{Bytes: mediaUUID, Valid: true}, ID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
Title: req.Title, Title: req.Title,
Author: pgtype.Text{String: req.Author, Valid: req.Author != ""}, Author: pgtype.Text{String: req.Author, Valid: req.Author != ""},
@@ -1297,7 +1308,17 @@ func (mh *MediaHandler) UpdateMediaItem(c *echo.Context) error {
AlternateInfo: alternateInfoBytes, AlternateInfo: alternateInfoBytes,
ScanInformation: pgtype.Text{String: req.ScanInformation, Valid: req.ScanInformation != ""}, ScanInformation: pgtype.Text{String: req.ScanInformation, Valid: req.ScanInformation != ""},
Summary: pgtype.Text{String: req.Summary, Valid: req.Summary != ""}, 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 { if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) 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) scanner := services.NewMediaScanner(mh.db)
defer scanner.Close() 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()}) 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") 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) coverFullPath, err := mh.libraryService.ResolveMediaPath(c.Request().Context(), mediaItem.LibraryID, coverRelPath)
if err != nil { if err != nil {
+40 -22
View File
@@ -157,18 +157,18 @@ type CalibreOPFMetadata struct {
// never closed. // never closed.
func NewMediaScanner(db *database.Queries) *MediaScanner { func NewMediaScanner(db *database.Queries) *MediaScanner {
return &MediaScanner{ return &MediaScanner{
db: db, db: db,
watcher: nil, watcher: nil,
settingsCache: NewSettingsCache(30 * time.Second), settingsCache: NewSettingsCache(30 * time.Second),
dirtyDirs: make(map[string]time.Time), dirtyDirs: make(map[string]time.Time),
fileStability: make(map[string]*atomic.Bool), fileStability: make(map[string]*atomic.Bool),
watching: atomic.Bool{}, watching: atomic.Bool{},
scanInProgress: atomic.Bool{}, scanInProgress: atomic.Bool{},
folders: []string{}, folders: []string{},
adminID: pgtype.UUID{}, adminID: pgtype.UUID{},
defaultLibraryID: pgtype.UUID{Valid: false}, defaultLibraryID: pgtype.UUID{Valid: false},
libraryTypes: make(map[string][]string), libraryTypes: make(map[string][]string),
logger: NewScannerLogger(), logger: NewScannerLogger(),
} }
} }
@@ -697,7 +697,7 @@ func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool,
if s.forceRescan { if s.forceRescan {
fmt.Printf("Force rescan enabled, updating existing media item: %s\n", path) fmt.Printf("Force rescan enabled, updating existing media item: %s\n", path)
// Use UPDATE instead of DELETE+INSERT to preserve created_at // 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) fmt.Printf("Warning: failed to update existing media item: %v\n", err)
} }
// Recompute hash identifiers too - a force rescan is the admin's // 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) // Normal behavior: check if file has changed (by size)
if existingItem.FileSize.Int64 != info.Size() { if existingItem.FileSize.Int64 != info.Size() {
fmt.Printf("File size changed, updating media item: %s\n", path) 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. // The bytes changed, so any stored hash is stale.
s.recomputeHashInfo(ctx, existingItem.ID, libraryID, path) s.recomputeHashInfo(ctx, existingItem.ID, libraryID, path)
return false, nil 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", fmt.Printf("Media item with same SHA-256 already exists in library (path %q), skipping duplicate: %s\n",
existingByHash.FilePath, path) existingByHash.FilePath, path)
if s.forceRescan { if s.forceRescan {
_ = s.updateMediaItem(ctx, existingByHash.ID, path, info) _ = s.updateMediaItem(ctx, existingByHash, path)
} }
return false, nil return false, nil
} else if err != nil && !errors.Is(err, pgx.ErrNoRows) { } else if err != nil && !errors.Is(err, pgx.ErrNoRows) {
@@ -2605,7 +2605,11 @@ func countArchiveImages(filePath string) (int, error) {
return count, nil 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 // Re-extract metadata for the update
metadata, err := s.extractMetadata(path) metadata, err := s.extractMetadata(path)
if err != nil { if err != nil {
@@ -2624,8 +2628,8 @@ func (s *MediaScanner) updateMediaItem(ctx context.Context, mediaItemID pgtype.U
if metadata.AlternateInfo != "" { if metadata.AlternateInfo != "" {
alternateInfoBytes = []byte(metadata.AlternateInfo) alternateInfoBytes = []byte(metadata.AlternateInfo)
} }
_, err = s.db.UpdateMediaItem(ctx, database.UpdateMediaItemParams{ params := database.UpdateMediaItemParams{
ID: mediaItemID, ID: existing.ID,
Title: metadata.Title, Title: metadata.Title,
Author: pgtype.Text{String: metadata.Author, Valid: metadata.Author != ""}, Author: pgtype.Text{String: metadata.Author, Valid: metadata.Author != ""},
Isbn: pgtype.Text{String: utils.NormalizeISBNSafe(metadata.ISBN), Valid: metadata.ISBN != ""}, 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, AlternateInfo: alternateInfoBytes,
ScanInformation: pgtype.Text{String: metadata.ScanInformation, Valid: metadata.ScanInformation != ""}, ScanInformation: pgtype.Text{String: metadata.ScanInformation, Valid: metadata.ScanInformation != ""},
Summary: pgtype.Text{String: metadata.Summary, Valid: metadata.Summary != ""}, Summary: pgtype.Text{String: metadata.Summary, Valid: metadata.Summary != ""},
}) }
// Keep user-customized fields, and keep the override set itself intact.
utils.ApplyMetadataOverrides(&params, existing)
params.MetadataOverrides = existing.MetadataOverrides
_, err = s.db.UpdateMediaItem(ctx, params)
return err return err
} }
// RescanMediaItem re-extracts metadata for a single media item and updates it. // 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 // It is the per-book rescan used by the Edit Metadata dialog and backfills
// covers for items imported before the PDF render fallback existed. // 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) item, err := s.db.GetMediaItem(ctx, mediaItemID)
if err != nil { if err != nil {
return fmt.Errorf("media item not found: %w", err) 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) folders, err := s.db.GetLibraryFolders(ctx, item.LibraryID)
if err != nil || len(folders) == 0 { if err != nil || len(folders) == 0 {
return fmt.Errorf("no library folders found for library") 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) return fmt.Errorf("media file not found on disk: %s", item.FilePath)
} }
info, err := os.Stat(fullPath) if _, err := os.Stat(fullPath); err != nil {
if err != nil {
return fmt.Errorf("failed to stat media file: %w", err) 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) return fmt.Errorf("failed to update media item: %w", err)
} }
s.recomputeHashInfo(ctx, mediaItemID, item.LibraryID, fullPath) s.recomputeHashInfo(ctx, mediaItemID, item.LibraryID, fullPath)
+305
View File
@@ -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
}
}
+112
View File
@@ -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(&params, 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)
}
}