Remove redundant type conversions that Go 1.26 makes unnecessary or that
were already no-ops:
- uuid.UUID(x.Bytes) → x.Bytes (uuid.UUID is [16]byte, same as pgtype UUID Bytes)
- pgtype.UUID{Bytes: [16]byte(u), Valid: true} → pgtype.UUID{Bytes: u, Valid: true}
- (*time.Time)(&x.Time) → &x.Time
- json.RawMessage(x) → x where x is already []byte
- []byte(stringVal) → stringVal where []byte is expected
- int()/int64()/byte() casts on values already of the target type
- Decompressor(fn) → fn (type is identical)
Handle previously ignored error returns:
- collections.go: check json.Unmarshal error in GetCollection
- conversion_service.go: check fileSize.Scan() error
- app_test.go: check app.Shutdown() error in benchmark
336 lines
10 KiB
Go
336 lines
10 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bookhoard/internal/services"
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/labstack/echo/v5"
|
|
)
|
|
|
|
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: 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: libraryID, Valid: true}, pgtype.UUID{Bytes: 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: 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
|
|
}
|