diff --git a/database/schema/schema.sql b/database/schema/schema.sql index d7a1cba..bdfccd0 100644 --- a/database/schema/schema.sql +++ b/database/schema/schema.sql @@ -213,6 +213,13 @@ CREATE TABLE IF NOT EXISTS media_items ( -- reset-to-scanned-defaults action clears them. metadata_overrides TEXT[] NOT NULL DEFAULT '{}', + -- Archive lifecycle: a file missing from disk for two consecutive scans + -- gets archived_at set (hidden from all browsing, reading history kept). + -- If the file returns, the row is un-archived. Archived rows are purged + -- after ARCHIVE_RETENTION_DAYS (0 = manual purge only). + missing_scan_count INT NOT NULL DEFAULT 0, + archived_at TIMESTAMPTZ, + -- Chapter metadata for reader navigation and progress tracking -- Caches detected chapter structure to avoid re-parsing files -- Populated by ReaderService.DetectChapters() on first read @@ -243,6 +250,11 @@ ALTER TABLE media_items ADD COLUMN IF NOT EXISTS contributors_search TEXT[]; -- definition above for fresh databases) ALTER TABLE media_items ADD COLUMN IF NOT EXISTS metadata_overrides TEXT[] NOT NULL DEFAULT '{}'; +-- Archive lifecycle (idempotent backfill for existing installs) +ALTER TABLE media_items ADD COLUMN IF NOT EXISTS missing_scan_count INT NOT NULL DEFAULT 0; +ALTER TABLE media_items ADD COLUMN IF NOT EXISTS archived_at TIMESTAMPTZ; +CREATE INDEX IF NOT EXISTS idx_media_items_archived ON media_items (archived_at) WHERE archived_at IS NOT NULL; + -- Create GIN indexes for fast search field searches CREATE INDEX IF NOT EXISTS idx_media_items_tags_search ON media_items USING GIN (tags_search); CREATE INDEX IF NOT EXISTS idx_media_items_contributors_search ON media_items USING GIN (contributors_search); diff --git a/internal/database/models.go b/internal/database/models.go index 5af8871..480f944 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -305,16 +305,18 @@ type MediaItems struct { // Scan information from ComicInfo.xml (scanner group, resolution, etc.) ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` // Summary from ComicInfo.xml (may be merged with description from Calibre) - Summary pgtype.Text `db:"summary" json:"summary"` - MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` - ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` - LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` - TagsSearch []string `db:"tags_search" json:"tags_search"` - ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` - FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` - OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"` - OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"` - HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"` + Summary pgtype.Text `db:"summary" json:"summary"` + MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` + MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` + ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` + ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` + LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` + TagsSearch []string `db:"tags_search" json:"tags_search"` + ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` + FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` + OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"` + OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"` + HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"` } type MediaNotes struct { diff --git a/internal/database/querier.go b/internal/database/querier.go index 4ae6b0a..65d1795 100644 --- a/internal/database/querier.go +++ b/internal/database/querier.go @@ -20,6 +20,9 @@ type Querier interface { AddBookToKoboShelf(ctx context.Context, arg AddBookToKoboShelfParams) (KoboShelves, error) // Library Folders queries AddLibraryFolder(ctx context.Context, arg AddLibraryFolderParams) (LibraryFolders, error) + // Second consecutive missing scan: hide the item from all browsing while + // preserving reading history in case the file returns. + ArchiveMediaItem(ctx context.Context, id pgtype.UUID) error // Bulk update format group for all media items BulkUpdateFormatGroups(ctx context.Context) error BulkUpdateProgressFromSync(ctx context.Context, arg BulkUpdateProgressFromSyncParams) ([]interface{}, error) @@ -30,10 +33,14 @@ type Querier interface { ClearDeviceSyncQueue(ctx context.Context, deviceID pgtype.UUID) error ClearKoboShelf(ctx context.Context, deviceID pgtype.UUID) error ClearKoboShelfByName(ctx context.Context, arg ClearKoboShelfByNameParams) error + // File is back on disk (by path or content hash): restore visibility and + // reset the missing-scan counter. No-op for items that were never archived. + ClearMediaItemArchive(ctx context.Context, id pgtype.UUID) 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) + CountArchivedMediaItems(ctx context.Context) (int64, error) // Count unlinked books for a device CountUnlinkedBooks(ctx context.Context, deviceID pgtype.UUID) (int64, error) CountUserDevices(ctx context.Context, userID pgtype.UUID) (int64, error) @@ -343,6 +350,7 @@ type Querier interface { ListLibraries(ctx context.Context) ([]ListLibrariesRow, error) ListMediaItems(ctx context.Context, arg ListMediaItemsParams) ([]ListMediaItemsRow, error) ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype.UUID) ([]ListMediaItemsByLibraryRow, error) + ListMediaItemsByLibraryIncludingArchived(ctx context.Context, libraryID pgtype.UUID) ([]ListMediaItemsByLibraryIncludingArchivedRow, error) // List all media items sharing a SHA-256 hash within a library (hash conflict group) ListMediaItemsBySHA256AndLibrary(ctx context.Context, arg ListMediaItemsBySHA256AndLibraryParams) ([]MediaItems, error) // List media items that have no stored SHA-256 (imported before hashing existed) @@ -356,6 +364,13 @@ type Querier interface { // List unresolved unlinked books with pagination ListUnresolvedUnlinkedBooks(ctx context.Context, arg ListUnresolvedUnlinkedBooksParams) ([]ListUnresolvedUnlinkedBooksRow, error) ListUsers(ctx context.Context) ([]ListUsersRow, error) + // First consecutive scan that cannot find the file on disk. + MarkMediaItemMissing(ctx context.Context, id pgtype.UUID) error + // Manual bulk purge from the library admin page. + PurgeAllArchivedMediaItems(ctx context.Context) ([]PurgeAllArchivedMediaItemsRow, error) + // Retention sweep at library-scan time: hard-delete archived items older + // than the cutoff. Cascades remove reading history with the row. + PurgeExpiredArchivedMediaItems(ctx context.Context, archivedAt pgtype.Timestamptz) ([]PurgeExpiredArchivedMediaItemsRow, error) PurgeExpiredBookmarkTombstones(ctx context.Context, deletedAt pgtype.Timestamptz) error PurgeExpiredHighlightTombstones(ctx context.Context, deletedAt pgtype.Timestamptz) error PurgeExpiredNoteTombstones(ctx context.Context, deletedAt pgtype.Timestamptz) error diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index 18b8576..065d519 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -107,6 +107,18 @@ func (q *Queries) AddLibraryFolder(ctx context.Context, arg AddLibraryFolderPara return i, err } +const ArchiveMediaItem = `-- name: ArchiveMediaItem :exec +UPDATE media_items SET archived_at = NOW(), updated_at = NOW() +WHERE id = $1 +` + +// Second consecutive missing scan: hide the item from all browsing while +// preserving reading history in case the file returns. +func (q *Queries) ArchiveMediaItem(ctx context.Context, id pgtype.UUID) error { + _, err := q.db.Exec(ctx, ArchiveMediaItem, id) + return err +} + const BulkUpdateFormatGroups = `-- name: BulkUpdateFormatGroups :exec UPDATE media_items m SET @@ -226,6 +238,18 @@ func (q *Queries) ClearKoboShelfByName(ctx context.Context, arg ClearKoboShelfBy return err } +const ClearMediaItemArchive = `-- name: ClearMediaItemArchive :exec +UPDATE media_items SET archived_at = NULL, missing_scan_count = 0, updated_at = NOW() +WHERE id = $1 AND (archived_at IS NOT NULL OR missing_scan_count > 0) +` + +// File is back on disk (by path or content hash): restore visibility and +// reset the missing-scan counter. No-op for items that were never archived. +func (q *Queries) ClearMediaItemArchive(ctx context.Context, id pgtype.UUID) error { + _, err := q.db.Exec(ctx, ClearMediaItemArchive, id) + return err +} + const ClearMediaItemMetadataOverrides = `-- name: ClearMediaItemMetadataOverrides :exec UPDATE media_items SET metadata_overrides = '{}', updated_at = NOW() WHERE id = $1 @@ -249,6 +273,17 @@ func (q *Queries) CountAdmins(ctx context.Context) (int64, error) { return count, err } +const CountArchivedMediaItems = `-- name: CountArchivedMediaItems :one +SELECT COUNT(*) FROM media_items WHERE archived_at IS NOT NULL +` + +func (q *Queries) CountArchivedMediaItems(ctx context.Context) (int64, error) { + row := q.db.QueryRow(ctx, CountArchivedMediaItems) + var count int64 + err := row.Scan(&count) + return count, err +} + const CountUnlinkedBooks = `-- name: CountUnlinkedBooks :one SELECT COUNT(*) as count FROM unlinked_books @@ -912,7 +947,7 @@ const CreateMediaItem = `-- name: CreateMediaItem :one INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publisher, contributors, contributors_search, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, library_type_name) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44) ON CONFLICT (library_id, file_path) DO UPDATE SET updated_at = NOW() -RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, 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, missing_scan_count, archived_at, chapter_metadata, library_type_name, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence ` type CreateMediaItemParams struct { @@ -1066,6 +1101,8 @@ func (q *Queries) CreateMediaItem(ctx context.Context, arg CreateMediaItemParams &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -2460,7 +2497,7 @@ func (q *Queries) GetAnnotationsForBook(ctx context.Context, arg GetAnnotationsF } const GetBooksByTag = `-- name: GetBooksByTag :many -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, 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, missing_scan_count, archived_at, chapter_metadata, library_type_name, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE library_id = $1 AND tags @> ARRAY[$2::text] ORDER BY title ASC ` @@ -2534,6 +2571,8 @@ func (q *Queries) GetBooksByTag(ctx context.Context, arg GetBooksByTagParams) ([ &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -2633,7 +2672,7 @@ func (q *Queries) GetCollectionItems(ctx context.Context, collectionID pgtype.UU } const GetCollectionItemsForDashboard = `-- name: GetCollectionItemsForDashboard :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.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.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, ci.excluded FROM media_items mi INNER JOIN collection_items ci ON ci.media_item_id = mi.id WHERE ci.collection_id = $1 AND ($2::uuid IS NULL OR mi.library_id = $2::uuid) @@ -2702,6 +2741,8 @@ type GetCollectionItemsForDashboardRow struct { ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` Summary pgtype.Text `db:"summary" json:"summary"` MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` + MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` + ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` TagsSearch []string `db:"tags_search" json:"tags_search"` @@ -2777,6 +2818,8 @@ func (q *Queries) GetCollectionItemsForDashboard(ctx context.Context, arg GetCol &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -2929,7 +2972,7 @@ func (q *Queries) GetCollectionsForBook(ctx context.Context, mediaItemID pgtype. } const GetContinueReadingItems = `-- name: GetContinueReadingItems :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.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.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi INNER JOIN ( SELECT DISTINCT ON (media_item_id) media_item_id, last_read_at FROM reading_progress @@ -3014,6 +3057,8 @@ func (q *Queries) GetContinueReadingItems(ctx context.Context, arg GetContinueRe &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -3047,15 +3092,16 @@ WITH user_series_progress AS ( GROUP BY mi.series ), next_books AS ( - SELECT DISTINCT ON (mi.series) mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.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.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, usp.last_read_at FROM media_items mi JOIN user_series_progress usp ON mi.series = usp.series - WHERE ($3::uuid IS NULL OR mi.library_id = $3::uuid) + WHERE mi.archived_at IS NULL + AND ($3::uuid IS NULL OR mi.library_id = $3::uuid) AND (mi.series_number > usp.max_read_number OR usp.max_read_number IS NULL) ORDER BY mi.series, mi.series_number ASC NULLS LAST ) -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, 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, missing_scan_count, archived_at, chapter_metadata, library_type_name, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, last_read_at FROM next_books ORDER BY last_read_at DESC NULLS LAST LIMIT $1 ` @@ -3121,6 +3167,8 @@ type GetContinueSeriesItemsRow struct { ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` Summary pgtype.Text `db:"summary" json:"summary"` MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` + MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` + ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` TagsSearch []string `db:"tags_search" json:"tags_search"` @@ -3196,6 +3244,8 @@ func (q *Queries) GetContinueSeriesItems(ctx context.Context, arg GetContinueSer &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -4325,7 +4375,7 @@ func (q *Queries) GetLibraryFolders(ctx context.Context, libraryID pgtype.UUID) } const GetLibraryItems = `-- name: GetLibraryItems :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.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.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid) ORDER BY mi.created_at DESC ` @@ -4394,6 +4444,8 @@ func (q *Queries) GetLibraryItems(ctx context.Context, libraryID pgtype.UUID) ([ &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -4825,7 +4877,7 @@ func (q *Queries) GetMediaHighlights(ctx context.Context, arg GetMediaHighlights } const GetMediaItem = `-- name: GetMediaItem :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, 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, missing_scan_count, archived_at, 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) { @@ -4886,6 +4938,8 @@ func (q *Queries) GetMediaItem(ctx context.Context, id pgtype.UUID) (MediaItems, &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -4899,7 +4953,7 @@ func (q *Queries) GetMediaItem(ctx context.Context, id pgtype.UUID) (MediaItems, } const GetMediaItemByFilePath = `-- name: GetMediaItemByFilePath :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, 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, missing_scan_count, archived_at, 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 { @@ -4965,6 +5019,8 @@ func (q *Queries) GetMediaItemByFilePath(ctx context.Context, arg GetMediaItemBy &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -4978,7 +5034,7 @@ func (q *Queries) GetMediaItemByFilePath(ctx context.Context, arg GetMediaItemBy } const GetMediaItemByFilePathAnyLibrary = `-- name: GetMediaItemByFilePathAnyLibrary :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, 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, missing_scan_count, archived_at, 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) { @@ -5039,6 +5095,8 @@ func (q *Queries) GetMediaItemByFilePathAnyLibrary(ctx context.Context, filePath &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -5053,7 +5111,7 @@ func (q *Queries) GetMediaItemByFilePathAnyLibrary(ctx context.Context, filePath const GetMediaItemByFilePathForSync = `-- name: GetMediaItemByFilePathForSync :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, 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, missing_scan_count, archived_at, chapter_metadata, library_type_name, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_path = $1 ` // ============================================ @@ -5117,6 +5175,8 @@ func (q *Queries) GetMediaItemByFilePathForSync(ctx context.Context, filePath st &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -5130,7 +5190,7 @@ func (q *Queries) GetMediaItemByFilePathForSync(ctx context.Context, filePath st } const GetMediaItemByKoboContentId = `-- name: GetMediaItemByKoboContentId :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, 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, missing_scan_count, archived_at, 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) { @@ -5191,6 +5251,8 @@ func (q *Queries) GetMediaItemByKoboContentId(ctx context.Context, koboContentID &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -5204,7 +5266,7 @@ func (q *Queries) GetMediaItemByKoboContentId(ctx context.Context, koboContentID } const GetMediaItemByOPFIdentifier = `-- name: GetMediaItemByOPFIdentifier :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, 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 +SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, 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 @@ -5266,6 +5328,8 @@ func (q *Queries) GetMediaItemByOPFIdentifier(ctx context.Context, opfIdentifier &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -5279,7 +5343,7 @@ func (q *Queries) GetMediaItemByOPFIdentifier(ctx context.Context, opfIdentifier } const GetMediaItemByOPFUUID = `-- name: GetMediaItemByOPFUUID :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, 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, missing_scan_count, archived_at, 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 @@ -5341,6 +5405,8 @@ func (q *Queries) GetMediaItemByOPFUUID(ctx context.Context, opfUuid pgtype.Text &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -5354,7 +5420,7 @@ func (q *Queries) GetMediaItemByOPFUUID(ctx context.Context, opfUuid pgtype.Text } const GetMediaItemBySHA256 = `-- name: GetMediaItemBySHA256 :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, 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, missing_scan_count, archived_at, 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 @@ -5416,6 +5482,8 @@ func (q *Queries) GetMediaItemBySHA256(ctx context.Context, fileSha256 pgtype.Te &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -5429,7 +5497,7 @@ func (q *Queries) GetMediaItemBySHA256(ctx context.Context, fileSha256 pgtype.Te } const GetMediaItemBySHA256AndLibrary = `-- name: GetMediaItemBySHA256AndLibrary :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, 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, missing_scan_count, archived_at, 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 { @@ -5496,6 +5564,8 @@ func (q *Queries) GetMediaItemBySHA256AndLibrary(ctx context.Context, arg GetMed &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -5836,7 +5906,7 @@ func (q *Queries) GetNextRetryTime(ctx context.Context) (interface{}, error) { } const GetNotStartedItems = `-- name: GetNotStartedItems :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.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.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid) AND NOT EXISTS ( SELECT 1 FROM reading_progress rp @@ -5918,6 +5988,8 @@ func (q *Queries) GetNotStartedItems(ctx context.Context, arg GetNotStartedItems &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -6237,7 +6309,7 @@ func (q *Queries) GetReadingSpeed(ctx context.Context, arg GetReadingSpeedParams } const GetRecentlyAddedItems = `-- name: GetRecentlyAddedItems :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.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.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid) ORDER BY mi.imported_at DESC NULLS LAST, mi.created_at DESC LIMIT $2 @@ -6312,6 +6384,8 @@ func (q *Queries) GetRecentlyAddedItems(ctx context.Context, arg GetRecentlyAdde &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -6332,7 +6406,7 @@ func (q *Queries) GetRecentlyAddedItems(ctx context.Context, arg GetRecentlyAdde } const GetRecentlyReadItems = `-- name: GetRecentlyReadItems :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.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.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi INNER JOIN ( SELECT DISTINCT ON (media_item_id) media_item_id, last_read_at FROM reading_progress @@ -6415,6 +6489,8 @@ func (q *Queries) GetRecentlyReadItems(ctx context.Context, arg GetRecentlyReadI &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -6535,7 +6611,7 @@ func (q *Queries) GetSavedFilters(ctx context.Context, arg GetSavedFiltersParams } const GetSeriesBooks = `-- name: GetSeriesBooks :many -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, 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, missing_scan_count, archived_at, chapter_metadata, library_type_name, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE series = $1 ORDER BY series_number ASC NULLS LAST ` @@ -6604,6 +6680,8 @@ func (q *Queries) GetSeriesBooks(ctx context.Context, series pgtype.Text) ([]Med &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -7805,7 +7883,7 @@ const GetVisibleLibraryMediaCounts = `-- name: GetVisibleLibraryMediaCounts :man SELECT l.id, COUNT(mi.id) as media_count FROM libraries l LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1 -LEFT JOIN media_items mi ON mi.library_id = l.id +LEFT JOIN media_items mi ON mi.library_id = l.id AND mi.archived_at IS NULL WHERE COALESCE(lv.is_visible, true) = true GROUP BY l.id ` @@ -8387,10 +8465,11 @@ func (q *Queries) ListLibraries(ctx context.Context) ([]ListLibrariesRow, error) } const ListMediaItems = `-- name: ListMediaItems :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.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.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id +WHERE mi.archived_at IS NULL ORDER BY mi.created_at DESC LIMIT $1 OFFSET $2 ` @@ -8454,6 +8533,8 @@ type ListMediaItemsRow struct { ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` Summary pgtype.Text `db:"summary" json:"summary"` MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` + MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` + ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` TagsSearch []string `db:"tags_search" json:"tags_search"` @@ -8530,6 +8611,8 @@ func (q *Queries) ListMediaItems(ctx context.Context, arg ListMediaItemsParams) &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -8552,11 +8635,12 @@ func (q *Queries) ListMediaItems(ctx context.Context, arg ListMediaItemsParams) } const ListMediaItemsByLibrary = `-- name: ListMediaItemsByLibrary :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.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.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id WHERE mi.library_id = $1 + AND mi.archived_at IS NULL ORDER BY mi.created_at DESC ` @@ -8615,6 +8699,8 @@ type ListMediaItemsByLibraryRow struct { ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` Summary pgtype.Text `db:"summary" json:"summary"` MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` + MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` + ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` TagsSearch []string `db:"tags_search" json:"tags_search"` @@ -8691,6 +8777,173 @@ func (q *Queries) ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype. &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, + &i.ChapterMetadata, + &i.LibraryTypeName, + &i.TagsSearch, + &i.ContributorsSearch, + &i.FileSha256, + &i.OpfIdentifier, + &i.OpfUuid, + &i.HashConfidence, + &i.LibraryName, + &i.LibraryTypeName_2, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const ListMediaItemsByLibraryIncludingArchived = `-- name: ListMediaItemsByLibraryIncludingArchived :many +SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name +FROM media_items mi +JOIN libraries l ON mi.library_id = l.id +JOIN library_types lt ON l.library_type_id = lt.id +WHERE mi.library_id = $1 +ORDER BY mi.created_at DESC +` + +type ListMediaItemsByLibraryIncludingArchivedRow struct { + ID pgtype.UUID `db:"id" json:"id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + Title string `db:"title" json:"title"` + Author pgtype.Text `db:"author" json:"author"` + Isbn pgtype.Text `db:"isbn" json:"isbn"` + Description pgtype.Text `db:"description" json:"description"` + FilePath string `db:"file_path" json:"file_path"` + FileSize pgtype.Int8 `db:"file_size" json:"file_size"` + MimeType pgtype.Text `db:"mime_type" json:"mime_type"` + CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` + Series pgtype.Text `db:"series" json:"series"` + SeriesNumber pgtype.Int4 `db:"series_number" json:"series_number"` + Tags []string `db:"tags" json:"tags"` + Asin pgtype.Text `db:"asin" json:"asin"` + DatePublished pgtype.Date `db:"date_published" json:"date_published"` + Publisher pgtype.Text `db:"publisher" json:"publisher"` + Contributors []string `db:"contributors" json:"contributors"` + Language pgtype.Text `db:"language" json:"language"` + Edition pgtype.Text `db:"edition" json:"edition"` + PageCount pgtype.Int4 `db:"page_count" json:"page_count"` + Genre pgtype.Text `db:"genre" json:"genre"` + CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"` + GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"` + OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"` + GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"` + AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + ImportedAt pgtype.Timestamptz `db:"imported_at" json:"imported_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + FormatGroup string `db:"format_group" json:"format_group"` + FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"` + IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"` + HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"` + TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"` + ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"` + EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"` + RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"` + KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"` + KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"` + MangaType pgtype.Text `db:"manga_type" json:"manga_type"` + ReadingDirection pgtype.Text `db:"reading_direction" json:"reading_direction"` + SeriesCount pgtype.Int4 `db:"series_count" json:"series_count"` + Volume pgtype.Int4 `db:"volume" json:"volume"` + Imprint pgtype.Text `db:"imprint" json:"imprint"` + AgeRating pgtype.Text `db:"age_rating" json:"age_rating"` + WebUrl pgtype.Text `db:"web_url" json:"web_url"` + StoryArc pgtype.Text `db:"story_arc" json:"story_arc"` + IsBlackAndWhite pgtype.Bool `db:"is_black_and_white" json:"is_black_and_white"` + MetadataNotes pgtype.Text `db:"metadata_notes" json:"metadata_notes"` + CommunityRating pgtype.Float8 `db:"community_rating" json:"community_rating"` + AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` + ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` + Summary pgtype.Text `db:"summary" json:"summary"` + MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` + MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` + ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` + ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` + LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` + TagsSearch []string `db:"tags_search" json:"tags_search"` + ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` + FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` + OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"` + OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"` + HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"` + LibraryName string `db:"library_name" json:"library_name"` + LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"` +} + +func (q *Queries) ListMediaItemsByLibraryIncludingArchived(ctx context.Context, libraryID pgtype.UUID) ([]ListMediaItemsByLibraryIncludingArchivedRow, error) { + rows, err := q.db.Query(ctx, ListMediaItemsByLibraryIncludingArchived, libraryID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListMediaItemsByLibraryIncludingArchivedRow{} + for rows.Next() { + var i ListMediaItemsByLibraryIncludingArchivedRow + if err := rows.Scan( + &i.ID, + &i.LibraryID, + &i.Title, + &i.Author, + &i.Isbn, + &i.Description, + &i.FilePath, + &i.FileSize, + &i.MimeType, + &i.CoverImagePath, + &i.Series, + &i.SeriesNumber, + &i.Tags, + &i.Asin, + &i.DatePublished, + &i.Publisher, + &i.Contributors, + &i.Language, + &i.Edition, + &i.PageCount, + &i.Genre, + &i.CopyrightYear, + &i.GoodreadsID, + &i.OpenlibraryID, + &i.GoogleBooksID, + &i.AddedByAdminID, + &i.CreatedAt, + &i.ImportedAt, + &i.UpdatedAt, + &i.FormatGroup, + &i.FormatMimetype, + &i.IsReflowable, + &i.HasFixedLayout, + &i.TotalCharacters, + &i.ChapterCount, + &i.EntitlementID, + &i.RevisionNumber, + &i.KoboContentID, + &i.KoboMetadata, + &i.MangaType, + &i.ReadingDirection, + &i.SeriesCount, + &i.Volume, + &i.Imprint, + &i.AgeRating, + &i.WebUrl, + &i.StoryArc, + &i.IsBlackAndWhite, + &i.MetadataNotes, + &i.CommunityRating, + &i.AlternateInfo, + &i.ScanInformation, + &i.Summary, + &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -8713,7 +8966,7 @@ func (q *Queries) ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype. } const ListMediaItemsBySHA256AndLibrary = `-- name: ListMediaItemsBySHA256AndLibrary :many -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, 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 +SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, 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 { @@ -8786,6 +9039,8 @@ func (q *Queries) ListMediaItemsBySHA256AndLibrary(ctx context.Context, arg List &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -8806,7 +9061,7 @@ func (q *Queries) ListMediaItemsBySHA256AndLibrary(ctx context.Context, arg List } const ListMediaItemsMissingHash = `-- name: ListMediaItemsMissingHash :many -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, 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, missing_scan_count, archived_at, 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) @@ -8874,6 +9129,8 @@ func (q *Queries) ListMediaItemsMissingHash(ctx context.Context) ([]MediaItems, &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -8894,11 +9151,12 @@ func (q *Queries) ListMediaItemsMissingHash(ctx context.Context) ([]MediaItems, } const ListMediaItemsSorted = `-- name: ListMediaItemsSorted :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.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.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id -WHERE mi.library_id = $1 +WHERE mi.archived_at IS NULL + AND mi.library_id = $1 ORDER BY CASE WHEN $2 = 'title ASC' THEN mi.title @@ -9030,6 +9288,8 @@ type ListMediaItemsSortedRow struct { ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` Summary pgtype.Text `db:"summary" json:"summary"` MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` + MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` + ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` TagsSearch []string `db:"tags_search" json:"tags_search"` @@ -9111,6 +9371,8 @@ func (q *Queries) ListMediaItemsSorted(ctx context.Context, arg ListMediaItemsSo &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -9532,6 +9794,82 @@ func (q *Queries) ListUsers(ctx context.Context) ([]ListUsersRow, error) { return items, nil } +const MarkMediaItemMissing = `-- name: MarkMediaItemMissing :exec +UPDATE media_items SET missing_scan_count = missing_scan_count + 1, updated_at = NOW() +WHERE id = $1 +` + +// First consecutive scan that cannot find the file on disk. +func (q *Queries) MarkMediaItemMissing(ctx context.Context, id pgtype.UUID) error { + _, err := q.db.Exec(ctx, MarkMediaItemMissing, id) + return err +} + +const PurgeAllArchivedMediaItems = `-- name: PurgeAllArchivedMediaItems :many +DELETE FROM media_items +WHERE archived_at IS NOT NULL +RETURNING id, title +` + +type PurgeAllArchivedMediaItemsRow struct { + ID pgtype.UUID `db:"id" json:"id"` + Title string `db:"title" json:"title"` +} + +// Manual bulk purge from the library admin page. +func (q *Queries) PurgeAllArchivedMediaItems(ctx context.Context) ([]PurgeAllArchivedMediaItemsRow, error) { + rows, err := q.db.Query(ctx, PurgeAllArchivedMediaItems) + if err != nil { + return nil, err + } + defer rows.Close() + items := []PurgeAllArchivedMediaItemsRow{} + for rows.Next() { + var i PurgeAllArchivedMediaItemsRow + if err := rows.Scan(&i.ID, &i.Title); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const PurgeExpiredArchivedMediaItems = `-- name: PurgeExpiredArchivedMediaItems :many +DELETE FROM media_items +WHERE archived_at IS NOT NULL AND archived_at < $1 +RETURNING id, title +` + +type PurgeExpiredArchivedMediaItemsRow struct { + ID pgtype.UUID `db:"id" json:"id"` + Title string `db:"title" json:"title"` +} + +// Retention sweep at library-scan time: hard-delete archived items older +// than the cutoff. Cascades remove reading history with the row. +func (q *Queries) PurgeExpiredArchivedMediaItems(ctx context.Context, archivedAt pgtype.Timestamptz) ([]PurgeExpiredArchivedMediaItemsRow, error) { + rows, err := q.db.Query(ctx, PurgeExpiredArchivedMediaItems, archivedAt) + if err != nil { + return nil, err + } + defer rows.Close() + items := []PurgeExpiredArchivedMediaItemsRow{} + for rows.Next() { + var i PurgeExpiredArchivedMediaItemsRow + if err := rows.Scan(&i.ID, &i.Title); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const PurgeExpiredBookmarkTombstones = `-- name: PurgeExpiredBookmarkTombstones :exec DELETE FROM media_bookmarks WHERE deleted = TRUE AND deleted_at < $1 ` @@ -10068,6 +10406,7 @@ FROM media_items mi JOIN libraries l ON mi.library_id = l.id LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2 WHERE COALESCE(lv.is_visible, true) = true + AND mi.archived_at IS NULL AND mi.library_id = $3 AND word_similarity($1, COALESCE(mi.author, '')) > 0.3 AND mi.author IS NOT NULL @@ -10126,6 +10465,7 @@ FROM media_items mi JOIN libraries l ON mi.library_id = l.id LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2 WHERE COALESCE(lv.is_visible, true) = true + AND mi.archived_at IS NULL AND mi.library_id = $3 AND word_similarity($1, COALESCE(mi.genre, '')) > 0.3 AND mi.genre IS NOT NULL @@ -10184,6 +10524,7 @@ FROM media_items mi JOIN libraries l ON mi.library_id = l.id LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2 WHERE COALESCE(lv.is_visible, true) = true + AND mi.archived_at IS NULL AND mi.library_id = $3 AND word_similarity($1, COALESCE(mi.language, '')) > 0.3 AND mi.language IS NOT NULL @@ -10234,12 +10575,13 @@ func (q *Queries) SearchLanguageValues(ctx context.Context, arg SearchLanguageVa } const SearchMediaItems = `-- name: SearchMediaItems :many -SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.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.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1 WHERE COALESCE(lv.is_visible, true) = true + AND mi.archived_at IS NULL AND ($2::uuid IS NULL OR mi.library_id = $2::uuid) AND ( mi.title ILIKE $3 OR @@ -10323,6 +10665,8 @@ type SearchMediaItemsRow struct { ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` Summary pgtype.Text `db:"summary" json:"summary"` MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` + MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` + ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` TagsSearch []string `db:"tags_search" json:"tags_search"` @@ -10406,6 +10750,8 @@ func (q *Queries) SearchMediaItems(ctx context.Context, arg SearchMediaItemsPara &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -10480,6 +10826,7 @@ JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1 WHERE COALESCE(lv.is_visible, true) = true + AND mi.archived_at IS NULL AND ($2::uuid IS NULL OR mi.library_id = $2::uuid) -- Fuzzy author filter @@ -10764,6 +11111,7 @@ FROM media_items mi JOIN libraries l ON mi.library_id = l.id LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2 WHERE COALESCE(lv.is_visible, true) = true + AND mi.archived_at IS NULL AND mi.library_id = $3 AND word_similarity($1, COALESCE(mi.series, '')) > 0.3 AND mi.series IS NOT NULL @@ -11880,7 +12228,7 @@ UPDATE media_items SET metadata_overrides = $38, updated_at = NOW() WHERE id = $1 -RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, 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, missing_scan_count, archived_at, chapter_metadata, library_type_name, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence ` type UpdateMediaItemParams struct { @@ -12021,6 +12369,8 @@ func (q *Queries) UpdateMediaItem(ctx context.Context, arg UpdateMediaItemParams &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -12037,7 +12387,7 @@ const UpdateMediaItemChapterMetadata = `-- name: UpdateMediaItemChapterMetadata UPDATE media_items SET chapter_metadata = $2, updated_at = NOW() WHERE id = $1 -RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, 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, missing_scan_count, archived_at, chapter_metadata, library_type_name, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence ` type UpdateMediaItemChapterMetadataParams struct { @@ -12103,6 +12453,8 @@ func (q *Queries) UpdateMediaItemChapterMetadata(ctx context.Context, arg Update &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -12210,7 +12562,7 @@ SET hash_confidence = $5, updated_at = NOW() WHERE id = $1 -RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, 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, missing_scan_count, archived_at, chapter_metadata, library_type_name, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence ` type UpdateMediaItemIdentifiersParams struct { @@ -12290,6 +12642,8 @@ func (q *Queries) UpdateMediaItemIdentifiers(ctx context.Context, arg UpdateMedi &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, @@ -12310,7 +12664,7 @@ SET entitlement_id = $2, kobo_metadata = $5, updated_at = NOW() WHERE id = $1 -RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, 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, missing_scan_count, archived_at, chapter_metadata, library_type_name, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence ` type UpdateMediaItemKoboMetadataParams struct { @@ -12385,6 +12739,8 @@ func (q *Queries) UpdateMediaItemKoboMetadata(ctx context.Context, arg UpdateMed &i.ScanInformation, &i.Summary, &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, &i.TagsSearch, diff --git a/internal/database/queries/queries.sql b/internal/database/queries/queries.sql index 6586edc..a66fc86 100644 --- a/internal/database/queries/queries.sql +++ b/internal/database/queries/queries.sql @@ -141,7 +141,7 @@ ORDER BY l.created_at ASC; SELECT l.id, COUNT(mi.id) as media_count FROM libraries l LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1 -LEFT JOIN media_items mi ON mi.library_id = l.id +LEFT JOIN media_items mi ON mi.library_id = l.id AND mi.archived_at IS NULL WHERE COALESCE(lv.is_visible, true) = true GROUP BY l.id; @@ -160,6 +160,7 @@ SELECT mi.*, l.name as library_name, lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id +WHERE mi.archived_at IS NULL ORDER BY mi.created_at DESC LIMIT $1 OFFSET $2; -- name: ListMediaItemsByLibrary :many @@ -167,6 +168,15 @@ SELECT mi.*, l.name as library_name, lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id +WHERE mi.library_id = $1 + AND mi.archived_at IS NULL +ORDER BY mi.created_at DESC; + +-- name: ListMediaItemsByLibraryIncludingArchived :many +SELECT mi.*, l.name as library_name, lt.name as library_type_name +FROM media_items mi +JOIN libraries l ON mi.library_id = l.id +JOIN library_types lt ON l.library_type_id = lt.id WHERE mi.library_id = $1 ORDER BY mi.created_at DESC; @@ -175,7 +185,8 @@ SELECT mi.*, l.name as library_name, lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id -WHERE mi.library_id = sqlc.narg('library_id') +WHERE mi.archived_at IS NULL + AND mi.library_id = sqlc.narg('library_id') ORDER BY CASE WHEN sqlc.narg('sort') = 'title ASC' THEN mi.title @@ -293,6 +304,39 @@ RETURNING *; UPDATE media_items SET metadata_overrides = '{}', updated_at = NOW() WHERE id = $1; +-- name: MarkMediaItemMissing :exec +-- First consecutive scan that cannot find the file on disk. +UPDATE media_items SET missing_scan_count = missing_scan_count + 1, updated_at = NOW() +WHERE id = $1; + +-- name: ArchiveMediaItem :exec +-- Second consecutive missing scan: hide the item from all browsing while +-- preserving reading history in case the file returns. +UPDATE media_items SET archived_at = NOW(), updated_at = NOW() +WHERE id = $1; + +-- name: ClearMediaItemArchive :exec +-- File is back on disk (by path or content hash): restore visibility and +-- reset the missing-scan counter. No-op for items that were never archived. +UPDATE media_items SET archived_at = NULL, missing_scan_count = 0, updated_at = NOW() +WHERE id = $1 AND (archived_at IS NOT NULL OR missing_scan_count > 0); + +-- name: PurgeExpiredArchivedMediaItems :many +-- Retention sweep at library-scan time: hard-delete archived items older +-- than the cutoff. Cascades remove reading history with the row. +DELETE FROM media_items +WHERE archived_at IS NOT NULL AND archived_at < $1 +RETURNING id, title; + +-- name: PurgeAllArchivedMediaItems :many +-- Manual bulk purge from the library admin page. +DELETE FROM media_items +WHERE archived_at IS NOT NULL +RETURNING id, title; + +-- name: CountArchivedMediaItems :one +SELECT COUNT(*) FROM media_items WHERE archived_at IS NOT NULL; + -- name: DeleteMediaItem :exec DELETE FROM media_items WHERE id = $1; @@ -441,6 +485,7 @@ JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id') WHERE COALESCE(lv.is_visible, true) = true + AND mi.archived_at IS NULL AND (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) AND ( mi.title ILIKE sqlc.narg('search_pattern') OR @@ -513,6 +558,7 @@ JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id') WHERE COALESCE(lv.is_visible, true) = true + AND mi.archived_at IS NULL AND (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) -- Fuzzy author filter @@ -646,6 +692,7 @@ FROM media_items mi JOIN libraries l ON mi.library_id = l.id LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id') WHERE COALESCE(lv.is_visible, true) = true + AND mi.archived_at IS NULL AND mi.library_id = sqlc.narg('library_id') AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.author, '')) > 0.3 AND mi.author IS NOT NULL @@ -663,6 +710,7 @@ FROM media_items mi JOIN libraries l ON mi.library_id = l.id LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id') WHERE COALESCE(lv.is_visible, true) = true + AND mi.archived_at IS NULL AND mi.library_id = sqlc.narg('library_id') AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.genre, '')) > 0.3 AND mi.genre IS NOT NULL @@ -697,6 +745,7 @@ FROM media_items mi JOIN libraries l ON mi.library_id = l.id LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id') WHERE COALESCE(lv.is_visible, true) = true + AND mi.archived_at IS NULL AND mi.library_id = sqlc.narg('library_id') AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.series, '')) > 0.3 AND mi.series IS NOT NULL @@ -714,6 +763,7 @@ FROM media_items mi JOIN libraries l ON mi.library_id = l.id LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id') WHERE COALESCE(lv.is_visible, true) = true + AND mi.archived_at IS NULL AND mi.library_id = sqlc.narg('library_id') AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.language, '')) > 0.3 AND mi.language IS NOT NULL @@ -2461,7 +2511,8 @@ next_books AS ( usp.last_read_at FROM media_items mi JOIN user_series_progress usp ON mi.series = usp.series - WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) + WHERE mi.archived_at IS NULL + AND (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid) 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 )