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
This commit is contained in:
2026-02-28 12:57:06 -05:00
parent 4d0d86838a
commit ce72781ec0
4 changed files with 72 additions and 44 deletions
-1
View File
@@ -10,7 +10,6 @@ services:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${DBPASS}
COOKIE_SECURE: false # make true in production with HTTPS
SCAN_POLL_INTERVAL_SECONDS: 30
volumes:
- postgres_data:/var/lib/postgresql/data
- ./database/schema:/docker-entrypoint-initdb.d
-2
View File
@@ -19,7 +19,6 @@ type Config struct {
TestMode bool
RateLimitEnabled bool
RequestsPerMinute int
ScanPollIntervalSeconds int `env:"SCAN_POLL_INTERVAL_SECONDS" default:"30"`
}
func LoadConfig() *Config {
@@ -37,7 +36,6 @@ func LoadConfig() *Config {
TestMode: getEnvBool("TEST_MODE", false),
RateLimitEnabled: getEnvBool("RATE_LIMIT_ENABLED", true),
RequestsPerMinute: getEnvInt("REQUESTS_PER_MINUTE", 10),
ScanPollIntervalSeconds: getEnvInt("SCAN_POLL_INTERVAL_SECONDS", 30),
}
}
+1 -4
View File
@@ -16,7 +16,6 @@ type Handler struct {
db *database.Queries
scanner *services.MediaScanner
worker *services.Worker
scheduler *services.Scheduler
queueProcessor *wsync.SyncQueueProcessor
queueCtx context.Context
queueCancel context.CancelFunc
@@ -34,7 +33,6 @@ type Handler struct {
func NewHandler(db *database.Queries, connManager *wsync.ConnectionManager, queueProcessor *wsync.SyncQueueProcessor, cfg *config.Config) *Handler {
ctx, cancel := context.WithCancel(context.Background())
worker := services.NewWorker(3)
scheduler := services.NewScheduler(worker, db)
queueCtx, queueCancel := context.WithCancel(context.Background())
@@ -42,9 +40,8 @@ func NewHandler(db *database.Queries, connManager *wsync.ConnectionManager, queu
return &Handler{
db: db,
scanner: services.NewMediaScanner(db, cfg.ScanPollIntervalSeconds),
scanner: services.NewMediaScanner(db),
worker: worker,
scheduler: scheduler,
queueProcessor: queueProcessor,
queueCtx: queueCtx,
queueCancel: queueCancel,
+47 -13
View File
@@ -90,18 +90,12 @@ type MediaScanner struct {
}
// NewMediaScanner creates a new media scanner instance
func NewMediaScanner(db *database.Queries, pollIntervalSeconds int) *MediaScanner {
func NewMediaScanner(db *database.Queries) *MediaScanner {
watcher, err := fsnotify.NewWatcher()
if err != nil {
panic(fmt.Sprintf("Failed to create file watcher: %v", err))
}
pollInterval := 30 * time.Second
if pollInterval > 0 {
pollInterval = time.Duration(pollIntervalSeconds) * time.Second
}
return &MediaScanner{
db: db,
watcher: watcher,
@@ -111,10 +105,44 @@ func NewMediaScanner(db *database.Queries, pollIntervalSeconds int) *MediaScanne
libraryTypes: make(map[string][]string),
logger: NewScannerLogger(),
eventQueue: make(chan string, 500),
pollInterval: pollInterval,
}
}
func (s *MediaScanner) GetPollInterval() time.Duration {
if s.db == nil {
return 30 * time.Second
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
setting, err := s.db.GetSystemSetting(ctx, "scan_poll_interval_seconds")
if err != nil || setting == "" {
return 30 * time.Second
}
seconds, err := strconv.Atoi(setting)
if err != nil || seconds < 1 {
return 30 * time.Second
}
return time.Duration(seconds) * time.Second
}
func (s *MediaScanner) GetAutoScanEnabled() bool {
if s.db == nil {
return true // default to enabled
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
setting, err := s.db.GetSystemSetting(ctx, "auto_scan_enabled")
if err != nil || setting == "" {
return true
}
enabled, err := strconv.ParseBool(setting)
if err != nil {
return true
}
return enabled
}
func (s *MediaScanner) SetAdminID(adminID pgtype.UUID) {
s.adminID = adminID
}
@@ -1654,26 +1682,32 @@ func (s *MediaScanner) handleFileDelete(ctx context.Context, filePath string) {
}
func (s *MediaScanner) StartPolling(ctx context.Context) {
if s.pollInterval <= 0 {
interval := s.GetPollInterval()
if interval <= 0 {
fmt.Println("Polling fallback disabled (interval = 0")
return
}
ticker := time.NewTicker(s.pollInterval)
defer ticker.Stop()
fmt.Printf("Polling fallback started with interval: %v\n", s.pollInterval)
fmt.Printf("Polling fallback started with interval: %v\n", interval)
for {
ticker := time.NewTicker(interval)
defer ticker.Stop()
select {
case <-ctx.Done():
fmt.Println("Polling fallback stopped")
return
case <-ticker.C:
fmt.Println("Running polling fallback sync...")
//Re-read interval each tick for dynamic updates
interval = s.GetPollInterval()
fmt.Printf("Running polling fallback sync (interval: %v)...\n", interval)
if err := s.SyncFilesystemWithDatabase(ctx); err != nil {
fmt.Printf("Polling sync error: %v\n", err)
}
}
}
}
func (s *MediaScanner) SyncFilesystemWithDatabase(ctx context.Context) error {
fmt.Println("[POLL-SYNC] Starting filesystem sync with database")
for _, folder := range s.folders {