Commit Graph
18 Commits
Author SHA1 Message Date
john-okeefe 83b40cb82a fix: replace empty mutex critical section with atomic scan tracking
Removes problematic empty critical section (lines 1993-1994) that
was intentionally waiting for mutex availability. Replaces with
atomic.Bool scan tracking to avoid linter warnings while maintaining
the same scan serialization behavior.

Old pattern:
  mu.Lock()
  // intentionally empty wait for mutex
  mu.Unlock()

New pattern:
  scanRunning atomic.Bool
  if !scanRunning.CompareAndSwap(false, true) {
      return ErrScanInProgress
  }
  defer scanRunning.Store(false)

This provides equivalent functionality with better performance
characteristics and clearer intent.
2026-03-24 16:47:36 -04:00
john-okeefe 821cd3df4c refactor(services): remove debug logging and fix directory scanning
- Remove debug printf statements from media scanner and worker
- Remove unused debug tracking variables (filesSeen, filesProcessed)
- Fix directory walk logic to properly scan the root directory itself
  (previous implementation would skip the root path entirely)

Clean up production code by removing debug artifacts and improving
the directory scanning logic to handle root-level directories correctly.
2026-03-06 10:48:36 -05: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 b3263b2611 feat: add settings cache to reduce database queries in MediaScanner
- Add SettingsCache with TTL-based invalidation (30 seconds)
- Cache scan_poll_interval_seconds and auto_scan_enabled settings
- Reduce database queries from every poll/check to once per TTL period
- Improve error handling with proper fallback values
- Simplify boolean parsing with strings.ToLower for consistency

This optimization reduces database load when checking scan settings,
which occurs frequently during media scanning operations.
2026-03-05 19:35:02 -05:00
john-okeefe 54bfd778db Refactor MediaScanner for improved file watching and job queue integration
- Replace event queue with dirty directories tracking (Jellyfin approach)
- Add file stability checking to wait for file writes to complete
- Add initial scan on startup to detect existing files
- Integrate with Worker job queue for directory scanning
- Change WatchChanges to return error and use atomic.Bool for state
- Add scan_mutex to prevent concurrent scans
- Add Close method with proper cleanup of resources
- Enhance polling with configurable interval
2026-03-05 16:28:40 -05:00
john-okeefe c6fa217092 feat: Add library ID support to media scanner and worker
Add default library ID functionality to improve library targeting
during media scans.

Service changes in internal/services/media_scanner.go:
- Add defaultLibraryID field to MediaScanner struct
- Add SetLibraryID() method to set default library
- Modify processMediaFile() to use defaultLibraryID when set
  - Prioritizes defaultLibraryID over folder-based library detection
  - Provides explicit library targeting for scans

Service changes in internal/services/worker.go:
- Add libraryUUID conversion from string to pgtype.UUID
- Call scanner.SetLibraryID() before ScanFolders()
  - Ensures scanner respects the job's library ID

These changes enable more precise library targeting during media scans,
allowing scans to be directed to specific libraries rather than relying
solely on folder-based detection.
2026-03-01 00:29:39 -05:00
john-okeefe ce72781ec0 refactor(scanner): make poll interval dynamic from database
- Add GetPollInterval() method to MediaScanner to read from database
- Add GetAutoScanEnabled() method to check if auto-scan is enabled
- Remove ScanPollIntervalSeconds from config (now DB-driven)
- Update NewMediaScanner signature to not require interval parameter
- Remove SCAN_POLL_INTERVAL_SECONDS from docker-compose env var
2026-02-28 12:57:06 -05:00
john-okeefe 286d0b5e06 feat(scanner): convert scan poll interval from minutes to seconds
- Rename SCAN_POLL_INTERVAL_MINUTES to SCAN_POLL_INTERVAL_SECONDS in config
- Update MediaScanner to accept interval in seconds instead of minutes
- Adjust default polling interval from 3 minutes to 30 seconds for faster response
- Add debug logging for fsnotify events to aid troubleshooting file watching

This change improves media file detection responsiveness by reducing the
polling interval from minutes to seconds, while maintaining the file
watcher as the primary detection mechanism.
2026-02-28 01:59:35 -05:00
john-okeefe 037e7c1189 feat(scanner): add debounced file watching with polling fallback
- 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
2026-02-28 01:16:27 -05:00
john-okeefe 209e9f2a3c feat: implement relative path storage and URL resolution for media files
- Add libraryService dependency to CollectionHandler and OPDSHandler for centralized path resolution
- Create internal/utils/mediaurl.go with ResolveMediaURL() function as single source of truth
- Update GetMediaItem and ListMediaItems handlers to return resolved URLs in API responses
- Update collection handlers (GetCollection, TestRules, PreviewCollection) to use resolved cover URLs
- Update progress handler (GetAllProgress) to use resolved cover URLs
- Add library_id to GetCollectionItems SQL query to enable URL resolution
- Refactor media scanner to store relative paths instead of absolute filesystem paths
- Add ResolveMediaPath() to LibraryService for resolving relative paths to absolute paths
- Add ServeFile endpoint at /uploads/library-:id/* for authenticated file serving
- Add MimeTypes map to library_service.go for consistent MIME type handling
- Update DownloadBook handler to use resolved filesystem paths
- Add getRelativePath() helper to MediaScanner for converting absolute to relative paths
- Use strings.EqualFold for case-insensitive path comparisons in zip extraction

This change enables the application to work with relative paths stored in the
database, making it portable across different server environments while
maintaining backward compatibility with existing absolute paths.
2026-02-27 16:51:44 -05:00
john-okeefe d802236874 scanner: fix library isolation, file mtime, force rescan, and deletion handling
Fix 1 - File modification time for created_at:
- Get file.ModTime() in processMediaFile and pass to CreateMediaItem
- Modified SQL INSERT to include created_at column

Fix 2 - Force rescan UPDATE instead of DELETE+INSERT:
- Changed force rescan logic to call updateMediaItem instead of delete + create
- Preserves created_at timestamp on force rescan

Fix 3 - GetMediaItemByFilePath filters by library_id:
- Added library_id to WHERE clause in SQL query
- Created GetMediaItemByFilePathAnyLibrary for cross-library lookups (KOReader)
- Added SetLibraryID method to MediaScanner
- Updated handler to call SetLibraryID for watch mode

Fix 4 - File deletion handling with persistent logging:
- Added fsnotify.Remove handler in WatchChanges
- Added orphan cleanup in ScanFolders after scan completes
- Created scanner_logger.go with daily log rotation (7 days)
- Logs to /app/logs/scanner-deletes-YYYY-MM-DD.log and scanner-errors-YYYY-MM-DD.log
- Individual deletes with enhanced safety logging

Note: Integration tests can now safely scan /app/uploads because
GetMediaItemByFilePath now filters by library_id, preventing
cross-library interference.
2026-02-26 16:39:42 -05:00
john-okeefe a97220e654 backend: add force rescan parameter to scanner
- Add Force bool field to ScanLibraryRequest in handlers
- Pass force param through job params to worker
- Add forceRescan field and SetForce method to MediaScanner
- Modify processMediaFile to delete and re-create existing items when force=true
- Default behavior unchanged (force=false maintains skip-if-exists)
2026-02-26 10:11:46 -05:00
john-okeefe 213e7b9a9d Add EPUB and PDF cover extraction with metadata support
- Add extractEPUBCover() to extract embedded covers from EPUB files
  - Parse OPF manifest for cover-image properties
  - Support meta name="cover" tags
  - Fall back to common cover paths (cover.jpg, images/cover.jpg)

- Add findSidecarCover() for sidecar cover detection
  - Check cover.jpg, cover.png, cover.webp
  - Check folder.jpg, folder.png
  - Check {basename}.jpg (same name as media file)

- Add extractPDFCover() to extract first page images from PDFs
  - Use pdfcpu API to extract images from page 1
  - Save largest image as cover

- Update extractPDFMetadata() to use pdfcpu API
  - Extract Title, Author, Subject, Creator, Producer
  - Call extractPDFCover for embedded covers
  - Fall back to sidecar covers

- Update extractMetadata() for EPUB to call extractEPUBCover
  - Try embedded cover first, then sidecar
2026-02-25 20:53:18 -05:00
john-okeefe 0295bf7a37 Implement backend scan progress tracking (Steps 1-7)
Implements comprehensive progress tracking for scan jobs to provide real-time
statistics to the frontend (files_scanned, new_items, errors).

Changes:
1. Extended JobResult struct with new fields:
   - FilesScanned: total files processed
   - NewItems: books added to database
   - Errors: scan errors encountered

2. Added progress callback mechanism:
   - Job.ProgressCallback function field for real-time updates
   - Job.UpdateProgress() method to trigger callbacks
   - Worker stores callback and updates JobResult during scan

3. MediaScanner now tracks statistics:
   - totalFiles, newItems, errors counters
   - GetStats() method to retrieve statistics
   - First pass counts total files for progress calculation
   - Batches progress updates every 10 files (reduces mutex contention)
   - Final update ensures 100% progress is reported

4. Updated processMediaFile signature:
   - Returns (bool, error) instead of (error)
   - true = new item created, false = existing/updated/error
   - Increments newItems counter when creating database entries
   - Updated WatchChanges to handle new return value

5. Worker job completion extracts stats:
   - Parses result map for files_scanned, new_items, errors
   - Stores in final JobResult for API response

6. GetScanStatus API response includes new fields:
   - files_scanned, new_items, errors now in JSON response
   - Frontend can display real-time progress

Design decisions:
- Batching every 10 files balances performance vs. granularity
- Thread-safe via worker mutex (w.mu.Lock/Unlock)
- Callback pattern decouples scanner from job management
- processMediaFile return type allows tracking new vs. updated items
- Maintains backward compatibility (uses || 0 fallbacks in frontend)

Testing:
- All code compiles successfully
- Follows service layer pattern (no business logic in handlers)
- No database schema changes
- Integration tests to be added in Step 8 (separate commit)

Files modified:
- internal/services/worker.go (JobResult, Job struct, processScanJob, processJob)
- internal/services/media_scanner.go (struct fields, GetStats, ScanFolders, processMediaFile)
- internal/handlers/scanner.go (GetScanStatus response)

Related: TASKS-backend-progress-tracking.md Steps 1-7
2026-02-25 10:52:53 -05:00
john-okeefe 807d7b36ef refactor: update sevenzip import to use vendored package
Updated import path from github.com/bodgit/sevenzip to
bookhoard/internal/sevenzip to use the vendored package.
2026-02-22 16:29:28 -05:00
john-okeefe 2706ae52c1 refactor: remove Phase X terminology from source code comments
Remove planning document phase references from code comments:

app_test.go:
- Remove Phase 5 references from 8 test function comments

querier.go & queries.sql.go:
- Remove Phase 1, 2, 3, 4, 6 references from section headers
- Clean up week numbers (Weeks 5-6, Week 3-4, etc.)

queries.sql:
- Remove Phase 4 references from Kobo queries

kobo.go:
- Remove Phase 6 references from ContentId mapping comments

progress.go:
- Remove Phase 1 reference from route comment

media_scanner.go & media_scanner_library_type_test.go:
- Remove Phase 2 references from library type scanning comments

schema.sql:
- Remove Phase 1, 2, 3, 4, 5, 7 references from table/section comments
- Clean up: Format Detection, Progress Tracking, Device Registry,
  Sync Queue, Conflict Resolution, Reading History, Indexes, etc.

test_helpers.go:
- Remove Phase 6 reference from handler setup comment

These phase numbers were from internal planning documents and have no
meaning in the codebase. Removing them makes the code self-documenting.
2026-02-13 21:50:29 -05:00
john-okeefe 0f8db2ab07 Add ISBN-10 to ISBN-13 validation and conversion
Enhance NormalizeISBN to validate and convert ISBNs:
- Validate length (10 or 13 digits), return error if invalid
- Convert ISBN-10 to ISBN-13 by prefixing '978' and recalculating checksum
- Add NormalizeISBNSafe for backward compatibility in scanners

This ensures all ISBNs stored in database are valid ISBN-13 format.
2026-02-11 09:42:18 -05:00
john-okeefe 0e813f14ce refactor(services): rename EbookScanner to MediaScanner
- Rename EbookScanner struct to MediaScanner
- Rename EbookMetadata struct to MediaMetadata
- Rename NewEbookScanner to NewMediaScanner
- Rename processEbookFile to processMediaFile
- Rename updateEbook to updateMediaItem
- Rename getEbookByFilePath to getMediaItemByFilePath
- Remove unused isEbookFile method
- Update all method receivers
- Update variable names (ebookFiles -> mediaFiles, existingEbook -> existingItem)
- Update print statements to use 'media' terminology
- Update worker.go to use NewMediaScanner
- File renamed: ebook_scanner.go -> media_scanner.go
2026-02-08 14:29:49 -05:00