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:
+21
-3
@@ -37,6 +37,7 @@ func main() {
|
||||
queries := database.New(dbPool)
|
||||
|
||||
authHandler := handlers.NewAuthHandler(queries, cfg.JWTSecret)
|
||||
libraryHandler := handlers.NewLibraryHandler(queries)
|
||||
|
||||
e := echo.New()
|
||||
|
||||
@@ -74,9 +75,26 @@ func main() {
|
||||
// Admin-only routes for user and folder management
|
||||
admin := protected.Group("/auth", handlers.AdminMiddleware)
|
||||
admin.GET("/users", authHandler.ListUsers)
|
||||
admin.POST("/ebook-folders", authHandler.AddEbookFolder)
|
||||
admin.GET("/ebook-folders", authHandler.GetEbookFolders)
|
||||
admin.DELETE("/ebook-folders", authHandler.DeleteEbookFolder)
|
||||
|
||||
// Library management routes
|
||||
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.PUT("/library/scan-settings", authHandler.UpdateScanSettings)
|
||||
|
||||
+266
-23
@@ -60,6 +60,17 @@ func SetupRoutes(g *echo.Group, db *database.Queries) {
|
||||
g.DELETE("/ebooks/:id/rating", h.DeleteEbookRating)
|
||||
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 := g.Group("", AdminMiddleware)
|
||||
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{
|
||||
EbookID: pgtype.UUID{Bytes: ebookId, Valid: true},
|
||||
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||
MediaItemID: pgtype.UUID{Bytes: ebookId, Valid: true},
|
||||
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
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{
|
||||
EbookID: pgtype.UUID{Bytes: ebookId, Valid: true},
|
||||
MediaItemID: pgtype.UUID{Bytes: ebookId, 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},
|
||||
@@ -396,9 +407,9 @@ func (h *Handler) CreateOrUpdateEbookRating(c echo.Context) error {
|
||||
}
|
||||
|
||||
rating, err := h.db.CreateEbookRating(c.Request().Context(), database.CreateEbookRatingParams{
|
||||
EbookID: pgtype.UUID{Bytes: ebookId, Valid: true},
|
||||
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||
Rating: req.Rating,
|
||||
MediaItemID: pgtype.UUID{Bytes: ebookId, 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()})
|
||||
@@ -423,8 +434,8 @@ func (h *Handler) DeleteEbookRating(c echo.Context) error {
|
||||
}
|
||||
|
||||
err = h.db.DeleteEbookRating(c.Request().Context(), database.DeleteEbookRatingParams{
|
||||
EbookID: pgtype.UUID{Bytes: ebookId, Valid: true},
|
||||
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||
MediaItemID: pgtype.UUID{Bytes: ebookId, Valid: true},
|
||||
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
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 {
|
||||
folderPaths = req.FolderPaths
|
||||
} else {
|
||||
// Get user's configured ebook folders
|
||||
folders, err := h.db.GetUserEbookFolders(c.Request().Context(), pgtype.UUID{Bytes: userUUID, Valid: true})
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
// 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"})
|
||||
}
|
||||
|
||||
// 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"})
|
||||
}
|
||||
|
||||
// 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"})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user