- Implement event queue with 3-second debouncing for file system events - Add configurable polling fallback (default 3 min) via SCAN_POLL_INTERVAL_MINUTES - Add SyncFilesystemWithDatabase to detect orphaned DB entries and new files - Integrate utils.ResolveMediaURL for consistent media file path resolution - Add COOKIE_SECURE env var with SameSite=LaxMode for session cookies - Update media handler to properly decode URL paths for file serving - Refactor scanner initialization to accept poll interval configuration
208 lines
6.3 KiB
Markdown
208 lines
6.3 KiB
Markdown
# Autoscanner Improvements - Implementation Plan
|
|
|
|
## Problem Statement
|
|
|
|
The current autoscanner has two issues:
|
|
|
|
1. **Bulk file detection unreliable**: When files are copied in bulk (e.g., 6 files at once), fsnotify only detects 1 or 0 files
|
|
2. **Delete detection broken**: When files are deleted from the filesystem, they remain in the database
|
|
|
|
### Root Causes
|
|
|
|
1. **fsnotify limitations**: The file system watcher can miss events during bulk file operations
|
|
2. **Relative path mismatch**: Delete detection uses absolute paths for database lookups, but database stores relative paths (partially fixed)
|
|
|
|
## Solution Architecture
|
|
|
|
```
|
|
[fsnotify Events] → [Event Queue] → [Debounce Timer (3s)] → [Process Queue]
|
|
↓
|
|
[Polling Fallback (3 min)] → [Full Sync Check] → [Add missing / Remove deleted]
|
|
```
|
|
|
|
Two complementary systems working together:
|
|
- **fsnotify + debounce**: Handles most file changes in real-time
|
|
- **Polling fallback**: Safety net that catches anything fsnotify misses
|
|
|
|
## Implementation Plan
|
|
|
|
### Phase 1: Add Debounce to fsnotify Handler
|
|
|
|
**File**: `internal/services/media_scanner.go`
|
|
|
|
**Changes**:
|
|
1. Create an event queue to accumulate fsnotify events
|
|
2. Add debounce timer (3 seconds) that resets on each new event
|
|
3. When timer fires, process all queued events in batch
|
|
4. Process each event: new files → scan, deleted files → remove from DB
|
|
|
|
**Implementation Details**:
|
|
- Use a buffered channel as the event queue
|
|
- Use `time.After()` or `time.Timer` for debounce
|
|
- Process events in order, skip duplicates for same file
|
|
|
|
```go
|
|
// Pseudo-code structure
|
|
type FileEvent struct {
|
|
path string
|
|
isDelete bool
|
|
}
|
|
|
|
eventQueue := make(chan FileEvent, 100)
|
|
var debounceTimer *time.Timer
|
|
|
|
func handleFsEvent(event fsnotify.Event) {
|
|
select {
|
|
case eventQueue <- FileEvent{path: event.Name, isDelete: event.Has(fsnotify.Remove)}:
|
|
default:
|
|
// Queue full, log warning
|
|
}
|
|
|
|
// Reset debounce timer
|
|
if debounceTimer != nil {
|
|
debounceTimer.Stop()
|
|
}
|
|
debounceTimer = time.AfterFunc(3*time.Second, processEventQueue)
|
|
}
|
|
|
|
func processEventQueue() {
|
|
// Drain queue and process unique paths
|
|
}
|
|
```
|
|
|
|
### Phase 2: Add Polling Fallback
|
|
|
|
**File**: `internal/services/media_scanner.go` (or new file)
|
|
|
|
**Changes**:
|
|
1. Add polling interval configuration (default: 3 minutes)
|
|
2. Create sync function that:
|
|
- Walks all library folders
|
|
- Compares filesystem against database
|
|
- Adds missing files (triggers scan for new files)
|
|
- Removes orphaned database entries (files no longer exist)
|
|
3. Start polling goroutine alongside existing fsnotify watcher
|
|
|
|
**Configuration**:
|
|
- Environment variable: `SCAN_POLL_INTERVAL_MINUTES` (default: 3)
|
|
- Use existing config system or add to `internal/config/config.go`
|
|
|
|
**Implementation Details**:
|
|
```go
|
|
type ScannerSyncOptions struct {
|
|
PollInterval time.Duration // default: 3 minutes
|
|
}
|
|
|
|
func (s *MediaScanner) StartPolling(ctx context.Context, opts ScannerSyncOptions) {
|
|
ticker := time.NewTicker(opts.PollInterval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
s.SyncFilesystemWithDatabase(ctx)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *MediaScanner) SyncFilesystemWithDatabase(ctx context.Context) error {
|
|
// 1. Get all media items from database
|
|
// 2. For each library folder:
|
|
// - Walk filesystem, build map of existing files (relative paths)
|
|
// - Compare against database
|
|
// - Add: file in filesystem but not in DB → scan
|
|
// - Remove: file in DB but not on filesystem → delete
|
|
}
|
|
```
|
|
|
|
### Phase 3: Reuse Existing Cleanup Logic
|
|
|
|
**File**: `internal/services/media_scanner.go`
|
|
|
|
The cleanup logic already exists (lines 252-299 in `ScanFolders`) - it removes orphaned items after each manual scan. We can refactor this into a reusable function called by both:
|
|
- Manual scan (existing behavior)
|
|
- Polling fallback (new behavior)
|
|
|
|
```go
|
|
func (s *MediaScanner) CleanupOrphanedItems(ctx context.Context) error {
|
|
// Existing cleanup code from ScanFolders
|
|
// Extract to reusable function
|
|
}
|
|
```
|
|
|
|
### Phase 4: Configuration
|
|
|
|
**File**: `internal/config/config.go`
|
|
|
|
Add new configuration option:
|
|
|
|
```go
|
|
type Config struct {
|
|
// ... existing fields ...
|
|
ScanPollIntervalMinutes int `env:"SCAN_POLL_INTERVAL_MINUTES"`
|
|
}
|
|
|
|
func LoadConfig() *Config {
|
|
return &Config{
|
|
// ... existing fields ...
|
|
ScanPollIntervalMinutes: getEnvInt("SCAN_POLL_INTERVAL_MINUTES", 3), // 3 minutes default
|
|
}
|
|
}
|
|
```
|
|
|
|
### Phase 5: Integration with Scheduler
|
|
|
|
**File**: `internal/services/scheduler.go`
|
|
|
|
The scheduler already handles periodic tasks. Consider:
|
|
1. Adding polling fallback to scheduler, OR
|
|
2. Starting polling directly in MediaScanner initialization
|
|
|
|
## Files to Modify
|
|
|
|
| File | Changes |
|
|
|------|---------|
|
|
| `internal/services/media_scanner.go` | Add debounce queue, polling fallback, reuse cleanup |
|
|
| `internal/config/config.go` | Add `SCAN_POLL_INTERVAL_MINUTES` config |
|
|
| `docker-compose.yml` | Add environment variable (optional) |
|
|
|
|
## Testing Plan
|
|
|
|
1. **Bulk file addition**: Copy 10+ files at once, verify all detected within 3 seconds (fsnotify) or 3 minutes (polling)
|
|
2. **Bulk file deletion**: Delete 5+ files, verify all removed from database within 3 minutes
|
|
3. **Mixed operations**: Add some, delete some, verify correct state
|
|
4. **Large library**: Test with 100+ files to ensure performance is acceptable
|
|
|
|
## Backward Compatibility
|
|
|
|
- Default polling interval: 3 minutes (user can configure)
|
|
- Existing manual scan functionality unchanged
|
|
- fsnotify continues to work as before (with debounce improvement)
|
|
|
|
## Security Considerations
|
|
|
|
- Polling runs on same goroutine as scanner (no new attack surface)
|
|
- File operations are read-only until changes detected
|
|
- Database operations use existing service layer (already authorized)
|
|
|
|
## Timeline Estimate
|
|
|
|
| Phase | Complexity | Estimate |
|
|
|-------|-------------|----------|
|
|
| Phase 1: Debounce | Medium | 1-2 hours |
|
|
| Phase 2: Polling | Medium | 1-2 hours |
|
|
| Phase 3: Reuse cleanup | Low | 30 min |
|
|
| Phase 4: Config | Low | 15 min |
|
|
| Phase 5: Integration | Low | 15 min |
|
|
| Testing | Medium | 1 hour |
|
|
| **Total** | - | **4-6 hours** |
|
|
|
|
## Future Improvements (Out of Scope)
|
|
|
|
1. **Configurable debounce duration**
|
|
2. **Per-library polling intervals**
|
|
3. **Event history/logging for debugging**
|
|
4. **Manual trigger for full sync**
|