From a9fdd4447166785d4e135be0ee9011b6968dd021 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Fri, 30 Jan 2026 20:16:22 -0500 Subject: [PATCH] Add library-based scanning and media item endpoints - Add POST /api/libraries/:id/scan endpoint for admin library scanning - Add GET /api/libraries/:id/media-items endpoint for library media items - Move /api/libraries/types to public endpoint (no auth required) - Update ScanEbooks handler to support library_id parameter --- cmd/server/main.go | 24 ++++++++++++++++---- internal/handlers/ebook.go | 45 +++++++++++++++++++++++++++++++------- 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index b75f31d..c18b4b3 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -115,6 +115,13 @@ func main() { // Protected routes protected := e.Group("/api", jwtMiddleware) + + // Setup ebook handler routes first (so we can use it for library scan) + h := handlers.SetupRoutes(protected, queries) + + // Public library types endpoint (no authentication required) + e.GET("/api/libraries/types", libraryHandler.GetLibraryTypes) + protected.GET("/auth/profile", authHandler.GetProfile) protected.PUT("/auth/profile", authHandler.UpdateProfile) protected.POST("/auth/refresh", authHandler.RefreshAccessToken) @@ -126,7 +133,6 @@ func main() { // Library management routes library := protected.Group("/libraries") - library.GET("/types", libraryHandler.GetLibraryTypes) // Admin-only library routes adminLibrary := library.Group("", handlers.AdminMiddleware) @@ -139,6 +145,19 @@ func main() { adminLibrary.GET("/:id/folders", libraryHandler.GetLibraryFolders) adminLibrary.DELETE("/:id/folders", libraryHandler.DeleteLibraryFolder) adminLibrary.GET("/:id/stats", libraryHandler.GetLibraryStats) + adminLibrary.POST("/:id/scan", func(c echo.Context) error { + libraryID := c.Param("id") + scanReq := map[string]interface{}{ + "library_id": libraryID, + } + c.Set("scan_request", scanReq) + return h.ScanEbooks(c) + }) + adminLibrary.GET("/:id/media-items", func(c echo.Context) error { + libraryID := c.Param("id") + c.QueryParams().Set("library_id", libraryID) + return h.ListMediaItems(c) + }) // User library visibility control protected.POST("/libraries/visibility", libraryHandler.SetLibraryVisibility) @@ -173,9 +192,6 @@ func main() { // Static files e.Static("/static", "web/static") - // Routes - h := handlers.SetupRoutes(protected, queries) - // Start scheduler for auto-scanning go h.StartScheduler() defer h.StopScheduler() diff --git a/internal/handlers/ebook.go b/internal/handlers/ebook.go index 22393c6..183f422 100644 --- a/internal/handlers/ebook.go +++ b/internal/handlers/ebook.go @@ -124,12 +124,22 @@ func SetupRoutes(g *echo.Group, db *database.Queries) *Handler { // ScanEbooksRequest represents the request for scanning ebooks type ScanEbooksRequest struct { FolderPaths []string `json:"folder_paths,omitempty"` + LibraryID string `json:"library_id,omitempty"` } // ScanEbooks handles POST /api/scanner/scan (now runs in background) func (h *Handler) ScanEbooks(c echo.Context) error { var req ScanEbooksRequest - if err := c.Bind(&req); err != nil { + + // 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"}) } @@ -142,14 +152,33 @@ func (h *Handler) ScanEbooks(c echo.Context) error { var folderPaths []string - // If folder paths provided in request, use them - // Otherwise, use user's saved folders - if len(req.FolderPaths) > 0 { + // 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 { - // TODO: Replace with library-based folder scanning - // For now, require folder paths in request - return c.JSON(http.StatusBadRequest, map[string]string{"error": "folder_paths required for scanning"}) + // 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() @@ -158,7 +187,7 @@ func (h *Handler) ScanEbooks(c echo.Context) error { ID: jobID, Type: services.JobTypeScan, Params: map[string]interface{}{ - "library_id": userID, + "library_id": req.LibraryID, "folders": folderPaths, "admin_id": userUUID.String(), "db": h.db,