package handlers import ( "bookhoard/internal/services" "context" "fmt" "net/http" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v4" ) const ( maxPaginationLimit = 1000 ) // StartBackgroundTasks starts the queue processor and cleanup task // This should be called once for the main handler instance func (h *Handler) StartBackgroundTasks() { h.cleanupTaskCancel = h.connManager.StartCleanupTask() go h.queueProcessor.Start(h.queueCtx) } // ScanLibraryRequest represents the request for scanning a library type ScanLibraryRequest struct { FolderPaths []string `json:"folder_paths,omitempty"` LibraryID string `json:"library_id,omitempty"` Force bool `json:"force,omitempty"` } // ScanLibrary handles POST /api/scanner/scan (now runs in background) func (h *Handler) ScanLibrary(c echo.Context) error { var req ScanLibraryRequest // Check if scan_request is set in context (from library scan route) if scanReq, ok := c.Get("scan_request").(map[string]interface{}); ok { if libraryID, ok := scanReq["library_id"].(string); ok { req.LibraryID = libraryID } } // Bind request body if provided (for direct scanner/scan calls) if err := c.Bind(&req); err != nil && req.LibraryID == "" { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) } // Get user ID from JWT token userID := c.Get("user_id").(string) userUUID, err := uuid.Parse(userID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"}) } var folderPaths []string // If library_id is provided, fetch folders from library if req.LibraryID != "" { libraryUUID, err := uuid.Parse(req.LibraryID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library id"}) } // Fetch library folders from database libraryFolders, err := h.db.GetLibraryFolders(c.Request().Context(), pgtype.UUID{Bytes: [16]byte(libraryUUID), Valid: true}) if err != nil { return c.JSON(http.StatusNotFound, map[string]string{"error": "library not found or has no folders"}) } // Extract folder paths for _, folder := range libraryFolders { folderPaths = append(folderPaths, folder.FolderPath) } if len(folderPaths) == 0 { return c.JSON(http.StatusBadRequest, map[string]string{"error": "library has no folders configured"}) } } else if len(req.FolderPaths) > 0 { // Use folder paths from request folderPaths = req.FolderPaths } else { // Neither library_id nor folder_paths provided return c.JSON(http.StatusBadRequest, map[string]string{"error": "either library_id or folder_paths required for scanning"}) } jobID := uuid.New().String() job := &services.Job{ ID: jobID, Type: services.JobTypeScan, UserID: userID, Params: map[string]interface{}{ "library_id": req.LibraryID, "folders": folderPaths, "admin_id": userUUID.String(), "db": h.db, "force": req.Force, }, Status: services.JobStatusPending, Context: h.ctx, } if err := h.worker.EnqueueJob(job); err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to enqueue scan job: " + err.Error()}) } return c.JSON(http.StatusAccepted, map[string]interface{}{ "message": "scan job enqueued", "job_id": jobID, "status": "pending", }) } // StartScanner handles POST /api/scanner/start func (h *Handler) StartScanner(c echo.Context) error { h.mu.Lock() defer h.mu.Unlock() var req ScanLibraryRequest if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) } // Get admin ID from JWT token userID := c.Get("user_id").(string) userUUID, err := uuid.Parse(userID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"}) } // Set the folder paths if err := h.scanner.SetFolders(req.FolderPaths); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid folder paths: " + err.Error()}) } // Set the admin ID for media item association h.scanner.SetAdminID(pgtype.UUID{Bytes: userUUID, Valid: true}) // Start watching for changes if !h.scanner.GetAutoScanEnabled() { return c.JSON(http.StatusBadRequest, map[string]string{"error": "auto-scan disabled in settings"}) } h.scanner.WatchChanges(h.watchModeCtx) return c.JSON(http.StatusOK, map[string]string{"message": "scanner started"}) } // StopScanner handles POST /api/scanner/stop func (h *Handler) StopScanner(c echo.Context) error { h.mu.Lock() defer h.mu.Unlock() h.watchModeCancel() h.watchModeCtx, h.watchModeCancel = context.WithCancel(context.Background()) return c.JSON(http.StatusOK, map[string]string{"message": "scanner stopped"}) } // GetScanStatus handles GET /api/scanner/status/:jobId func (h *Handler) GetScanStatus(c echo.Context) error { jobID := c.Param("jobId") result, exists := h.worker.GetJobStatus(jobID) if !exists { return c.JSON(http.StatusNotFound, map[string]string{"error": "job not found"}) } return c.JSON(http.StatusOK, map[string]interface{}{ "job_id": result.JobID, "status": result.Status, "error": result.Error, "result": result.Result, "progress": result.Progress, "files_scanned": result.FilesScanned, "new_items": result.NewItems, "errors": result.Errors, }) } // StartWatchModeForLibrary starts watching a specific library's folders func (h *Handler) StartWatchModeForLibrary(ctx context.Context, libraryID pgtype.UUID, adminID pgtype.UUID) error { h.mu.Lock() defer h.mu.Unlock() libraryIDStr := fmt.Sprintf("%x", libraryID.Bytes) if h.watchingLibraries[libraryIDStr] { return fmt.Errorf("already watching library %s", libraryIDStr) } folders, err := h.db.GetLibraryFolders(ctx, libraryID) if err != nil { return fmt.Errorf("failed to get library folders: %v", err) } if len(folders) == 0 { return fmt.Errorf("no folders configured for library") } folderPaths := make([]string, len(folders)) for i, folder := range folders { folderPaths[i] = folder.FolderPath } scanner := services.NewMediaScanner(h.db) if err := scanner.SetFolders(folderPaths); err != nil { return fmt.Errorf("failed to set scanner folders: %v", err) } scanner.SetAdminID(adminID) scanner.SetLibraryID(libraryID) scanner.WatchChanges(h.watchModeCtx) h.watchingLibraries[libraryIDStr] = true return nil } // StopWatchModeForLibrary stops watching a specific library func (h *Handler) StopWatchModeForLibrary(libraryID pgtype.UUID) error { h.mu.Lock() defer h.mu.Unlock() libraryIDStr := fmt.Sprintf("%x", libraryID.Bytes) if !h.watchingLibraries[libraryIDStr] { return fmt.Errorf("not watching library %s", libraryIDStr) } delete(h.watchingLibraries, libraryIDStr) if len(h.watchingLibraries) == 0 { h.watchModeCancel() newCtx, newCancel := context.WithCancel(context.Background()) h.watchModeCtx = newCtx h.watchModeCancel = newCancel } return nil } // StartWatchMode handles POST /api/scanner/watch/start func (h *Handler) StartWatchMode(c echo.Context) error { userID := c.Get("user_id").(string) userUUID, err := uuid.Parse(userID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user id"}) } var req struct { LibraryID string `json:"library_id"` } if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) } if req.LibraryID == "" { return c.JSON(http.StatusBadRequest, map[string]string{"error": "library_id required"}) } libraryID, err := uuid.Parse(req.LibraryID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library id"}) } if err := h.StartWatchModeForLibrary(c.Request().Context(), pgtype.UUID{Bytes: [16]byte(libraryID), Valid: true}, pgtype.UUID{Bytes: [16]byte(userUUID), Valid: true}); err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusOK, map[string]interface{}{ "message": "watch mode started for library", "library_id": req.LibraryID, }) } // StopWatchMode handles POST /api/scanner/watch/stop func (h *Handler) StopWatchMode(c echo.Context) error { var req struct { LibraryID string `json:"library_id"` } if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) } if req.LibraryID == "" { return c.JSON(http.StatusBadRequest, map[string]string{"error": "library_id required"}) } libraryID, err := uuid.Parse(req.LibraryID) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library id"}) } if err := h.StopWatchModeForLibrary(pgtype.UUID{Bytes: [16]byte(libraryID), Valid: true}); err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) } return c.JSON(http.StatusOK, map[string]interface{}{ "message": "watch mode stopped for library", "library_id": req.LibraryID, }) } // GetWatchModeStatus handles GET /api/scanner/watch/status func (h *Handler) GetWatchModeStatus(c echo.Context) error { h.mu.Lock() defer h.mu.Unlock() watchingLibraries := make([]string, 0, len(h.watchingLibraries)) for libID := range h.watchingLibraries { watchingLibraries = append(watchingLibraries, libID) } return c.JSON(http.StatusOK, map[string]interface{}{ "watching_libraries": watchingLibraries, "total_watching": len(watchingLibraries), }) } // StartWatchModeForAllLibraries starts watching all configured libraries func (h *Handler) StartWatchModeForAllLibraries(ctx context.Context) error { libraries, err := h.db.ListLibraries(ctx) if err != nil { return fmt.Errorf("failed to list libraries: %v", err) } for _, library := range libraries { libraryIDStr := fmt.Sprintf("%x", library.ID.Bytes) if err := h.StartWatchModeForLibrary(ctx, library.ID, library.CreatedByAdminID); err != nil { fmt.Printf("Warning: failed to start watch mode for library %s: %v\n", libraryIDStr, err) continue } fmt.Printf("Started watch mode for library %s (%s)\n", library.Name, libraryIDStr) } return nil }