feat(db): archive lifecycle schema and queries for missing media

Add the storage behind the archive-instead-of-delete lifecycle:

- media_items.missing_scan_count (INT, default 0) and archived_at
  (TIMESTAMPTZ, partial index), both as idempotent ADD COLUMN IF NOT
  EXISTS backfills for existing installs.
- MarkMediaItemMissing / ArchiveMediaItem / ClearMediaItemArchive plus
  PurgeExpiredArchivedMediaItems (retention cutoff) and
  PurgeAllArchivedMediaItems (manual bulk), with CountArchivedMediaItems
  for the admin UI.
- Archived items are hidden from every user-facing listing:
  ListMediaItems, ListMediaItemsByLibrary, ListMediaItemsSorted,
  SearchMediaItems, SearchMediaItemsUnified, the next_books CTE, the
  library media counts, and the search autocomplete value lists. Detail
  lookups by id/path/sha are intentionally unfiltered, and a dedicated
  ListMediaItemsByLibraryIncludingArchived feeds the scanner so the
  lifecycle pass can see and restore archived rows.
This commit is contained in:
John O'Keefe
2026-09-12 17:50:01 -04:00
parent 44b98f3fc3
commit e6aceae0da
5 changed files with 482 additions and 46 deletions
+12
View File
@@ -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);