diff --git a/docker-compose.yml b/docker-compose.yml index 613b3bd..8608f7f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/internal/config/config.go b/internal/config/config.go index e35a7c2..e3cb35d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -7,37 +7,35 @@ import ( ) type Config struct { - ServerPort string - BaseURL string - JWTSecret string - UploadPath string - DatabaseHost string - DatabasePort string - DatabaseUser string - DatabasePassword string - DatabaseName string - TestMode bool - RateLimitEnabled bool - RequestsPerMinute int - ScanPollIntervalSeconds int `env:"SCAN_POLL_INTERVAL_SECONDS" default:"30"` + ServerPort string + BaseURL string + JWTSecret string + UploadPath string + DatabaseHost string + DatabasePort string + DatabaseUser string + DatabasePassword string + DatabaseName string + TestMode bool + RateLimitEnabled bool + RequestsPerMinute int } func LoadConfig() *Config { port := getEnv("SERVER_PORT", "8765") return &Config{ - ServerPort: port, - BaseURL: getEnv("BASE_URL", "http://localhost:"+port), - DatabaseHost: getEnv("DATABASE_HOST", "localhost"), - DatabasePort: getEnv("DATABASE_PORT", "5432"), - DatabaseUser: getEnv("DATABASE_USER", "postgres"), - DatabasePassword: getEnv("DATABASE_PASSWORD", "password"), - DatabaseName: getEnv("DATABASE_NAME", "bookhoard"), - JWTSecret: getEnv("JWT_SECRET", "your-secret-key"), - UploadPath: getEnv("UPLOAD_PATH", "./uploads"), - TestMode: getEnvBool("TEST_MODE", false), - RateLimitEnabled: getEnvBool("RATE_LIMIT_ENABLED", true), - RequestsPerMinute: getEnvInt("REQUESTS_PER_MINUTE", 10), - ScanPollIntervalSeconds: getEnvInt("SCAN_POLL_INTERVAL_SECONDS", 30), + ServerPort: port, + BaseURL: getEnv("BASE_URL", "http://localhost:"+port), + DatabaseHost: getEnv("DATABASE_HOST", "localhost"), + DatabasePort: getEnv("DATABASE_PORT", "5432"), + DatabaseUser: getEnv("DATABASE_USER", "postgres"), + DatabasePassword: getEnv("DATABASE_PASSWORD", "password"), + DatabaseName: getEnv("DATABASE_NAME", "bookhoard"), + JWTSecret: getEnv("JWT_SECRET", "your-secret-key"), + UploadPath: getEnv("UPLOAD_PATH", "./uploads"), + TestMode: getEnvBool("TEST_MODE", false), + RateLimitEnabled: getEnvBool("RATE_LIMIT_ENABLED", true), + RequestsPerMinute: getEnvInt("REQUESTS_PER_MINUTE", 10), } } diff --git a/internal/handlers/commonhandlers.go b/internal/handlers/commonhandlers.go index 1f2da24..50cd6b5 100644 --- a/internal/handlers/commonhandlers.go +++ b/internal/handlers/commonhandlers.go @@ -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, diff --git a/internal/services/media_scanner.go b/internal/services/media_scanner.go index a2bf8ba..441afb5 100644 --- a/internal/services/media_scanner.go +++ b/internal/services/media_scanner.go @@ -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 {