feat(data): duplicate-content queries — archive filters, cross-library moves, archived-twin matching

Three related query groups in queries.sql (sqlc regenerated; no generated
signatures changed, so no caller edits were needed):

1. Dashboard/collection archive filters. The smart-section and collection
   queries returned archived and missing items (visible only as broken
   covers once their files vanished). Added the codebase-standard
   "AND archived_at IS NULL AND missing_scan_count = 0" predicate to:
   GetContinueReadingItems, GetRecentlyAddedItems, GetRecentlyReadItems,
   GetNotStartedItems, GetCollectionItemsForDashboard, GetLibraryItems.
   Matches the existing convention in ListMediaItems, SearchMediaItems,
   the library counts, and GetContinueSeriesItems.

2. Cross-library SHA move detection support.
   - GetMediaItemsBySHA256AndSameLibraryType: finds identical content
     (same file_sha256) registered in a DIFFERENT library of the SAME
     library type, ordered archived-first, most-missing, oldest. Type
     scoping keeps ebooks from merging into manga/comics libraries.
   - MoveMediaItemToLibrary: repoints an existing row to the new library
     (library_id + recomputed file_path + file_size) and clears
     archive/missing state. The row keeps its ID, so reading progress,
     annotations, collections, and kobo shelves follow the book
     automatically; the new set_library_type_name_on_library_change
     trigger refreshes the denormalized type column.

3. Archived-twin matching. ListHiddenMediaItems (admin archived-items
   page) now LEFT JOIN LATERALs the best active same-SHA twin per hidden
   row — preferring same library type, then same library, then most
   reading progress, then oldest — exposing match_id / match_title /
   match_library_name so the UI can offer merging the archived row's
   reading data into its active duplicate. COALESCE on match_title keeps
   the no-match case NULL-safe.
This commit is contained in:
John O'Keefe
2026-09-21 18:08:18 -04:00
parent 1002cbca93
commit a4dbe79d0b
3 changed files with 337 additions and 5 deletions
+15 -1
View File
@@ -259,6 +259,12 @@ type Querier interface {
GetMediaItemFormats(ctx context.Context, mediaItemID pgtype.UUID) ([]MediaItemFormats, error)
// Per-item user-data counts, used when choosing which duplicate copy to keep
GetMediaItemUsageCounts(ctx context.Context, mediaItemID pgtype.UUID) (GetMediaItemUsageCountsRow, error)
// Cross-library move detection: identical content registered in a different
// library of the same type (ebooks stay with ebooks, comics with comics).
// Candidates may be active copies (deliberate duplicates - leave alone) or
// hidden/archived rows whose file vanished (move candidates - the scanner
// verifies disk state before repointing).
GetMediaItemsBySHA256AndSameLibraryType(ctx context.Context, arg GetMediaItemsBySHA256AndSameLibraryTypeParams) ([]GetMediaItemsBySHA256AndSameLibraryTypeRow, error)
GetMediaNote(ctx context.Context, id pgtype.UUID) (MediaNotes, error)
// ============================================
// ANNOTATION SYNC QUERIES (notes)
@@ -349,7 +355,10 @@ type Querier interface {
ListDevicesByUser(ctx context.Context, userID pgtype.UUID) ([]Devices, error)
// Items hidden from libraries: either missing from disk (waiting on the
// second scan that archives them) or already archived and pending purge.
// Feeds the admin archived-items page.
// Feeds the admin archived-items page. match_* columns identify an active
// item with identical content (same SHA-256) so the UI can offer merging the
// archived row's reading data into its active twin instead of waiting for a
// file that will never return.
ListHiddenMediaItems(ctx context.Context) ([]ListHiddenMediaItemsRow, error)
ListLibraries(ctx context.Context) ([]ListLibrariesRow, error)
ListMediaItems(ctx context.Context, arg ListMediaItemsParams) ([]ListMediaItemsRow, error)
@@ -374,6 +383,11 @@ type Querier interface {
// a move. Repoints the row (and refreshes file-level fields) so reading
// history follows the book; archive/missing state is cleared separately.
MoveMediaItemFilePath(ctx context.Context, arg MoveMediaItemFilePathParams) error
// Cross-library move: identical content reappeared in a different library of
// the same type while the old file vanished. Repoints the row so reading
// history, annotations and collections follow the book; archive/missing
// state is cleared. library_type_name is refreshed by trigger.
MoveMediaItemToLibrary(ctx context.Context, arg MoveMediaItemToLibraryParams) 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