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
This commit is contained in:
2026-05-16 19:30:39 -04:00
parent 4b3433af7d
commit 13bb2975f8
3 changed files with 177 additions and 381 deletions
@@ -12,8 +12,8 @@ func TestMediaScanner_GetPollInterval(t *testing.T) {
settingsCache: NewSettingsCache(30 * time.Second),
}
interval := scanner.GetPollInterval()
if interval != 30*time.Minute {
t.Errorf("expected 30m, got %v", interval)
if interval != 5*time.Minute {
t.Errorf("expected 5m, got %v", interval)
}
})
}