feat: integrate library system with routing and handlers

- Add library routes to main router configuration
- Implement media items API endpoints for library content
- Update existing ebook handlers to use new schema
- Add media rating and progress tracking
- Maintain backward compatibility with existing endpoints
- Support library-specific media item queries

Updates application to support new multi-library architecture
This commit is contained in:
2026-01-28 11:02:51 -05:00
parent 87c0e309a3
commit e29054f841
2 changed files with 287 additions and 26 deletions
+21 -3
View File
@@ -37,6 +37,7 @@ func main() {
queries := database.New(dbPool) queries := database.New(dbPool)
authHandler := handlers.NewAuthHandler(queries, cfg.JWTSecret) authHandler := handlers.NewAuthHandler(queries, cfg.JWTSecret)
libraryHandler := handlers.NewLibraryHandler(queries)
e := echo.New() e := echo.New()
@@ -74,9 +75,26 @@ func main() {
// Admin-only routes for user and folder management // Admin-only routes for user and folder management
admin := protected.Group("/auth", handlers.AdminMiddleware) admin := protected.Group("/auth", handlers.AdminMiddleware)
admin.GET("/users", authHandler.ListUsers) admin.GET("/users", authHandler.ListUsers)
admin.POST("/ebook-folders", authHandler.AddEbookFolder)
admin.GET("/ebook-folders", authHandler.GetEbookFolders) // Library management routes
admin.DELETE("/ebook-folders", authHandler.DeleteEbookFolder) library := protected.Group("/libraries")
library.GET("/types", libraryHandler.GetLibraryTypes)
// Admin-only library routes
adminLibrary := library.Group("", handlers.AdminMiddleware)
adminLibrary.POST("", libraryHandler.CreateLibrary)
adminLibrary.GET("", libraryHandler.ListLibraries)
adminLibrary.GET("/:id", libraryHandler.GetLibrary)
adminLibrary.PUT("/:id", libraryHandler.UpdateLibrary)
adminLibrary.DELETE("/:id", libraryHandler.DeleteLibrary)
adminLibrary.POST("/:id/folders", libraryHandler.AddLibraryFolder)
adminLibrary.GET("/:id/folders", libraryHandler.GetLibraryFolders)
adminLibrary.DELETE("/:id/folders", libraryHandler.DeleteLibraryFolder)
adminLibrary.GET("/:id/stats", libraryHandler.GetLibraryStats)
// User library visibility control
protected.POST("/libraries/visibility", libraryHandler.SetLibraryVisibility)
protected.GET("/libraries/visible", libraryHandler.GetUserVisibleLibraries)
protected.DELETE("/auth/account", authHandler.DeleteAccount) protected.DELETE("/auth/account", authHandler.DeleteAccount)
protected.PUT("/library/scan-settings", authHandler.UpdateScanSettings) protected.PUT("/library/scan-settings", authHandler.UpdateScanSettings)
+266 -23
View File
@@ -60,6 +60,17 @@ func SetupRoutes(g *echo.Group, db *database.Queries) {
g.DELETE("/ebooks/:id/rating", h.DeleteEbookRating) g.DELETE("/ebooks/:id/rating", h.DeleteEbookRating)
g.GET("/ebooks/:id/ratings", h.GetEbookRatings) g.GET("/ebooks/:id/ratings", h.GetEbookRatings)
// Media item routes (new library system)
g.GET("/media-items", h.ListMediaItems)
g.GET("/media-items/:id", h.GetMediaItem)
g.POST("/media-items/:id/rating", h.CreateMediaRating)
g.GET("/media-items/:id/rating", h.GetMediaRating)
g.PUT("/media-items/:id/rating", h.UpdateMediaRating)
g.DELETE("/media-items/:id/rating", h.DeleteMediaRating)
g.GET("/media-items/:id/progress", h.GetMediaReadingProgress)
g.PUT("/media-items/:id/progress", h.UpdateMediaReadingProgress)
g.DELETE("/media-items/:id/progress", h.DeleteMediaReadingProgress)
// Admin-only routes // Admin-only routes
admin := g.Group("", AdminMiddleware) admin := g.Group("", AdminMiddleware)
admin.POST("/ebooks", h.CreateEbook) admin.POST("/ebooks", h.CreateEbook)
@@ -270,8 +281,8 @@ func (h *Handler) GetReadingProgress(c echo.Context) error {
} }
progress, err := h.db.GetReadingProgress(c.Request().Context(), database.GetReadingProgressParams{ progress, err := h.db.GetReadingProgress(c.Request().Context(), database.GetReadingProgressParams{
EbookID: pgtype.UUID{Bytes: ebookId, Valid: true}, MediaItemID: pgtype.UUID{Bytes: ebookId, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true}, UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
}) })
if err != nil { if err != nil {
if err == pgx.ErrNoRows { if err == pgx.ErrNoRows {
@@ -320,7 +331,7 @@ func (h *Handler) UpdateReadingProgress(c echo.Context) error {
} }
progress, err := h.db.UpdateReadingProgress(c.Request().Context(), database.UpdateReadingProgressParams{ progress, err := h.db.UpdateReadingProgress(c.Request().Context(), database.UpdateReadingProgressParams{
EbookID: pgtype.UUID{Bytes: ebookId, Valid: true}, MediaItemID: pgtype.UUID{Bytes: ebookId, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true}, UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
CurrentPage: pgtype.Int4{Int32: req.CurrentPage, Valid: true}, CurrentPage: pgtype.Int4{Int32: req.CurrentPage, Valid: true},
TotalPages: pgtype.Int4{Int32: req.TotalPages, Valid: req.TotalPages > 0}, TotalPages: pgtype.Int4{Int32: req.TotalPages, Valid: req.TotalPages > 0},
@@ -396,9 +407,9 @@ func (h *Handler) CreateOrUpdateEbookRating(c echo.Context) error {
} }
rating, err := h.db.CreateEbookRating(c.Request().Context(), database.CreateEbookRatingParams{ rating, err := h.db.CreateEbookRating(c.Request().Context(), database.CreateEbookRatingParams{
EbookID: pgtype.UUID{Bytes: ebookId, Valid: true}, MediaItemID: pgtype.UUID{Bytes: ebookId, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true}, UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
Rating: req.Rating, Rating: req.Rating,
}) })
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -423,8 +434,8 @@ func (h *Handler) DeleteEbookRating(c echo.Context) error {
} }
err = h.db.DeleteEbookRating(c.Request().Context(), database.DeleteEbookRatingParams{ err = h.db.DeleteEbookRating(c.Request().Context(), database.DeleteEbookRatingParams{
EbookID: pgtype.UUID{Bytes: ebookId, Valid: true}, MediaItemID: pgtype.UUID{Bytes: ebookId, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true}, UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
}) })
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()}) return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -476,21 +487,9 @@ func (h *Handler) ScanEbooks(c echo.Context) error {
if len(req.FolderPaths) > 0 { if len(req.FolderPaths) > 0 {
folderPaths = req.FolderPaths folderPaths = req.FolderPaths
} else { } else {
// Get user's configured ebook folders // TODO: Replace with library-based folder scanning
folders, err := h.db.GetUserEbookFolders(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true}) // For now, require folder paths in request
if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "folder_paths required for scanning"})
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to get user folders: " + err.Error()})
}
if len(folders) == 0 {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "no folders configured for user"})
}
// Convert to folder paths
folderPaths = make([]string, len(folders))
for i, folder := range folders {
folderPaths[i] = folder.FolderPath
}
} }
// Set the folder paths for scanning // Set the folder paths for scanning
@@ -550,3 +549,247 @@ func (h *Handler) StopScanner(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{"message": "scanner stopped"}) return c.JSON(http.StatusOK, map[string]string{"message": "scanner stopped"})
} }
// Media Item handlers for new library system
// ListMediaItems handles GET /api/media-items
func (h *Handler) ListMediaItems(c echo.Context) error {
libraryID := c.QueryParam("library_id")
limit, _ := strconv.Atoi(c.QueryParam("limit"))
offset, _ := strconv.Atoi(c.QueryParam("offset"))
if limit == 0 {
limit = 50
}
if libraryID != "" {
libUUID, err := uuid.Parse(libraryID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library id"})
}
items, err := h.db.ListMediaItemsByLibrary(c.Request().Context(), pgtype.UUID{Bytes: libUUID, Valid: true})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, items)
}
items, err := h.db.ListMediaItems(c.Request().Context(), database.ListMediaItemsParams{
Limit: int32(limit),
Offset: int32(offset),
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, items)
}
// GetMediaItem handles GET /api/media-items/:id
func (h *Handler) GetMediaItem(c echo.Context) error {
mediaID := c.Param("id")
mediaUUID, err := uuid.Parse(mediaID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
}
item, err := h.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: mediaUUID, Valid: true})
if err != nil {
if err == pgx.ErrNoRows {
return c.JSON(http.StatusNotFound, map[string]string{"error": "media item not found"})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, item)
}
// CreateMediaRating handles POST /api/media-items/:id/rating
func (h *Handler) CreateMediaRating(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"})
}
mediaID := c.Param("id")
mediaUUID, err := uuid.Parse(mediaID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
}
var req struct {
Rating int32 `json:"rating" validate:"required,min=1,max=10"`
}
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
}
if err := c.Validate(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
rating, err := h.db.CreateMediaRating(c.Request().Context(), database.CreateMediaRatingParams{
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
Rating: req.Rating,
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusCreated, rating)
}
// GetMediaRating handles GET /api/media-items/:id/rating
func (h *Handler) GetMediaRating(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"})
}
mediaID := c.Param("id")
mediaUUID, err := uuid.Parse(mediaID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
}
rating, err := h.db.GetMediaRating(c.Request().Context(), database.GetMediaRatingParams{
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
})
if err != nil {
if err == pgx.ErrNoRows {
return c.JSON(http.StatusOK, map[string]interface{}{"rating": nil})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, rating)
}
// UpdateMediaRating handles PUT /api/media-items/:id/rating
func (h *Handler) UpdateMediaRating(c echo.Context) error {
return h.CreateMediaRating(c) // Same logic as create due to upsert
}
// DeleteMediaRating handles DELETE /api/media-items/:id/rating
func (h *Handler) DeleteMediaRating(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"})
}
mediaID := c.Param("id")
mediaUUID, err := uuid.Parse(mediaID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
}
err = h.db.DeleteMediaRating(c.Request().Context(), database.DeleteMediaRatingParams{
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, map[string]string{"message": "rating deleted"})
}
// GetMediaReadingProgress handles GET /api/media-items/:id/progress
func (h *Handler) GetMediaReadingProgress(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"})
}
mediaID := c.Param("id")
mediaUUID, err := uuid.Parse(mediaID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
}
progress, err := h.db.GetReadingProgress(c.Request().Context(), database.GetReadingProgressParams{
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
})
if err != nil {
if err == pgx.ErrNoRows {
return c.JSON(http.StatusOK, map[string]interface{}{
"current_page": 0,
"total_pages": nil,
})
}
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, progress)
}
// UpdateMediaReadingProgress handles PUT /api/media-items/:id/progress
func (h *Handler) UpdateMediaReadingProgress(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"})
}
mediaID := c.Param("id")
mediaUUID, err := uuid.Parse(mediaID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
}
var req struct {
CurrentPage int32 `json:"current_page"`
TotalPages int32 `json:"total_pages"`
}
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
}
if err := c.Validate(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
}
progress, err := h.db.UpdateReadingProgress(c.Request().Context(), database.UpdateReadingProgressParams{
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
CurrentPage: pgtype.Int4{Int32: req.CurrentPage, Valid: true},
TotalPages: pgtype.Int4{Int32: req.TotalPages, Valid: req.TotalPages > 0},
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, progress)
}
// DeleteMediaReadingProgress handles DELETE /api/media-items/:id/progress
func (h *Handler) DeleteMediaReadingProgress(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"})
}
mediaID := c.Param("id")
mediaUUID, err := uuid.Parse(mediaID)
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
}
err = h.db.DeleteReadingProgress(c.Request().Context(), database.DeleteReadingProgressParams{
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
})
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return c.JSON(http.StatusOK, map[string]string{"message": "reading progress deleted"})
}