Commit Graph
5 Commits
Author SHA1 Message Date
John O'Keefe 91b0c45dd4 feat(scanner): extractor-version (ev) mechanism — parser fixes reach old rows without a force rescan (faceted plan Phase 10)
Parser fixes (7e4c97f6, genre model v2) never reached rows ingested
under an older parser: the sidecar never changed, so every fingerprint
said 'unchanged'. The manifest JSONB now carries the reserved 'ev'
stamp — the parser version that produced the item's row.

Semantics (all pinned by tests):
- ABSENT ev decodes as 1 ('v1-or-unknown'); stamping is truthful-only:
  create paths + post-extraction stores stamp the setting's current
  value; deploy-day backfill and the extract-failure guard stay UNSET
  (a row never claims a version its data can't prove — a mid-backfill
  bump self-heals next poll); repoint reconciliation preserves the
  stored ev (stat-only refresh re-extracted nothing).
- NEW setting scan_extractor_version (seeded + registry + getter,
  int default 1, category scanner): a manifest whose ev != the setting
  re-extracts through the SAME change path (full extraction →
  updateMediaItemWithMetadata → fresh stamped manifest). Sidecar-less
  books get an entries-less manifest as their ev home (create: stamped;
  first-sighting: unset).
- Catch-up bounded: evReextractBudgetPerScan=100, atomic (watcher pass
  can overlap a poll scan), reset per ScanFolders — amortized +
  resumable across polls; no force rescan, no flood.
- Multifile books included: the ev gate rides multifileSidecarChanged's
  shared ladder; ev-driven manifestChanged drives the editorial apply
  + stamped re-store.

Settings API: the unified GET/PUT /api/system/settings now lists the
key (registry-driven, auto-surfaces in the admin UI). Test harness:
the registry is now Loaded + wired into the SystemSettingsHandler
(main.go parity) — settings writes through the API were 503s before.

Verified: unit (ev normalization, budget CAS, GetExtractorVersion
cache paths, registry clamping); integration state machine —
create-stamp ev=1 → equality skips (corruption probe persists) →
wiped scan-state backfills UNSET without re-extract → bump re-extracts
ONCE (sidecar value restored, ev=2) → stable (second corruption
persists); multifile twin green; adjacent suites (Audiobook, Calibre,
CrossLibraryMove, SchemaIdempotency) green; live app restart re-scans
1198 files with 0 changed (deploy-day rule), setting listed at
/api/system/settings.
2026-10-20 18:24:08 -04:00
john-okeefe 0855dd7b3b fix(scanner): replace mtime polling with recursive fsnotify watching
The root cause of scanner failures in Podman containers was NOT that
inotify doesn't work through bind mounts (it does — same kernel, same
inodes). The real bug was SetFolders() only watching root directories.
Linux has no recursive inotify — every subdirectory must be added
individually to the watcher.

Changes:
- SetFolders() now walks all subdirectories and adds each to the watcher
  (same approach as Audiobookshelf/Kavita)
- Remove broken mtime-based detection: seedDirectoryMtimes,
  pollDirectoryChanges, detectChangedRoots, checkDirectoryMtimes,
  SyncFilesystemWithDatabase — all unreliable in container overlay mounts
- Replace StartPolling with startBackupScan: enqueues full JobTypeScan
  every 5 minutes (down from 30) as a safety-net fallback
- enqueueLibraryScan() sets job.UserID from admin ID so the worker can
  broadcast WebSocket messages
- performInitialScan() sets job.UserID for the same reason
- Add [WATCHER] prefix logging to all fsnotify event loop messages
- Add defense-in-depth: fallback to GetFirstAdmin() when library has
  no created_by_admin_id (NULL from test cleanup)
- Fix processDirectoryScanJob to use prefix-match (GetLibraryByFolderPathPrefix)
- Fix nil context panic: all jobs now set Context: context.Background()
- Remove mtime-related tests; update default interval test from 30m to 5m
2026-05-16 19:30:39 -04:00
john-okeefe ced90cd1f4 feat(scanner): add directory mtime-based fast polling for container environments
Podman rootless containers with overlay storage do not propagate inotify
events through bind mounts, making the fsnotify file watcher ineffective.
This caused new files added on the host to go undetected until the
5-minute full-filesystem-walk polling fallback caught them.

Add a lightweight directory mtime polling mechanism that runs every 10
seconds, checking stat() on all subdirectories under watched library
folders against a cached mtime value. When a directory's mtime changes
(indicating files were added/removed/renamed), it feeds into the existing
markDirectoryDirty() → processDirtyDirectories() → job queue pipeline.

Changes:
- Add dirMtimes cache + mutex to MediaScanner struct
- Add seedDirectoryMtimes() to populate cache on startup (prevents
  false-positive flood on first poll)
- Add pollDirectoryChanges() goroutine (10s ticker) and
  checkDirectoryMtimes() (walks directories, compares mtimes)
- Launch mtime poller from WatchChanges() alongside existing goroutines
- Rename StartPolling logs to [ORPHAN-CLEANUP] to clarify its role
- Change default poll interval from 60s → 30m (new file detection now
  handled by the fast mtime poll; full sync focuses on orphan cleanup)
- Update GetScanSettings default from 60 → 1800 seconds
- Add 5 tests: seed cache, skip nonexistent, detect new dir, skip
  unchanged, detect modified dir

Expected result: new files detected in ~20 seconds (10s poll + 10s
debounce) regardless of inotify/container support.
2026-05-12 16:54:35 -04:00
john-okeefe bb0158e8fb refactor: improve worker type safety and scanner reliability
Worker improvements:
- Add strongly-typed result structs for all job types
- Replace map[string]interface{} with specific result types
- Add JSON tags to JobResult for proper API serialization
- Fix processJob to handle different result types correctly
- Improve directory scan job with proper library folder resolution
- Add debug logging for scan operations

Media scanner improvements:
- Add nil checks for database in GetPollInterval and GetAutoScanEnabled
- Fix pdfcpu API call signature (add validateOnly parameter)
- Add debug logging for scanDirectory with file counters
- Improve error handling and reporting

Test fixes:
- Fix default poll interval expectation from 30s to 60s
- Add settingsCache initialization to scanner tests
- Add folders initialization to ProcessDirtyDirectories test
2026-03-06 01:52:42 -05:00
john-okeefe 4bf8e933df test: add unit and integration tests for scan settings
- Add unit tests for MediaScanner.GetPollInterval and GetAutoScanEnabled
- Add integration tests for scan-settings API endpoints
- Update validation test cases to use seconds (1-3600) instead of minutes
- Fix worker.go to use new NewMediaScanner signature
2026-02-28 14:09:06 -05:00