feat: add media handler for book download and shelf management
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bookmann/internal/database"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type MediaHandler struct {
|
||||
db *database.Queries
|
||||
}
|
||||
|
||||
func NewMediaHandler(db *database.Queries) *MediaHandler {
|
||||
return &MediaHandler{db: db}
|
||||
}
|
||||
|
||||
func (h *MediaHandler) DownloadBook(c echo.Context) error {
|
||||
bookUUID, err := uuid.Parse(c.Param("uuid"))
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid book UUID"})
|
||||
}
|
||||
|
||||
pgBookUUID := pgtype.UUID{Bytes: bookUUID, Valid: true}
|
||||
|
||||
mediaItem, err := h.db.GetMediaItem(c.Request().Context(), pgBookUUID)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": "book not found"})
|
||||
}
|
||||
|
||||
if _, err := os.Stat(mediaItem.FilePath); os.IsNotExist(err) {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": "book file not found on disk"})
|
||||
}
|
||||
|
||||
file, err := os.Open(mediaItem.FilePath)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to open book file"})
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
mimeType := mediaItem.MimeType.String
|
||||
if !mediaItem.MimeType.Valid || mimeType == "" {
|
||||
mimeType = mime.TypeByExtension(filepath.Ext(mediaItem.FilePath))
|
||||
}
|
||||
|
||||
c.Response().Header().Set("Content-Type", mimeType)
|
||||
c.Response().Header().Set("Content-Disposition", "attachment; filename=\""+filepath.Base(mediaItem.FilePath)+"\"")
|
||||
|
||||
if mediaItem.FileSize.Valid && mediaItem.FileSize.Int64 > 0 {
|
||||
c.Response().Header().Set("Content-Length", strconv.FormatInt(mediaItem.FileSize.Int64, 10))
|
||||
}
|
||||
|
||||
_, err = io.Copy(c.Response(), file)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to stream file"})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type AddToShelfRequest struct {
|
||||
MediaItemIDs []string `json:"media_item_ids" validate:"required"`
|
||||
ShelfName string `json:"shelf_name"`
|
||||
ShelfPosition int `json:"shelf_position"`
|
||||
}
|
||||
|
||||
func (h *MediaHandler) AddToShelf(c echo.Context) error {
|
||||
deviceUUID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid device ID"})
|
||||
}
|
||||
|
||||
var req AddToShelfRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request format"})
|
||||
}
|
||||
|
||||
if req.ShelfName == "" {
|
||||
req.ShelfName = "Default"
|
||||
}
|
||||
|
||||
pgDeviceUUID := pgtype.UUID{Bytes: deviceUUID, Valid: true}
|
||||
addedCount := 0
|
||||
|
||||
for _, mediaID := range req.MediaItemIDs {
|
||||
mediaUUID, err := uuid.Parse(mediaID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = h.db.AddBookToKoboShelf(c.Request().Context(), database.AddBookToKoboShelfParams{
|
||||
DeviceID: pgDeviceUUID,
|
||||
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
||||
ShelfName: pgtype.Text{String: req.ShelfName, Valid: true},
|
||||
ShelfPosition: pgtype.Int4{Int32: int32(req.ShelfPosition), Valid: true},
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
addedCount++
|
||||
req.ShelfPosition++
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"status": "success",
|
||||
"added_count": addedCount,
|
||||
"shelf_name": req.ShelfName,
|
||||
"total_count": len(req.MediaItemIDs),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *MediaHandler) GetShelf(c echo.Context) error {
|
||||
deviceUUID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid device ID"})
|
||||
}
|
||||
|
||||
shelfName := c.QueryParam("shelf")
|
||||
pgDeviceUUID := pgtype.UUID{Bytes: deviceUUID, Valid: true}
|
||||
|
||||
var shelfBooks []database.GetKoboShelfBooksByShelfNameRow
|
||||
if shelfName != "" {
|
||||
shelfBooks, err = h.db.GetKoboShelfBooksByShelfName(c.Request().Context(), database.GetKoboShelfBooksByShelfNameParams{
|
||||
DeviceID: pgDeviceUUID,
|
||||
ShelfName: pgtype.Text{String: shelfName, Valid: true},
|
||||
})
|
||||
} else {
|
||||
var defaultBooks []database.GetKoboShelfBooksRow
|
||||
defaultBooks, err = h.db.GetKoboShelfBooks(c.Request().Context(), pgDeviceUUID)
|
||||
shelfBooks = make([]database.GetKoboShelfBooksByShelfNameRow, len(defaultBooks))
|
||||
for i, b := range defaultBooks {
|
||||
shelfBooks[i] = database.GetKoboShelfBooksByShelfNameRow{
|
||||
ID: b.ID,
|
||||
DeviceID: b.DeviceID,
|
||||
MediaItemID: b.MediaItemID,
|
||||
ShelfName: b.ShelfName,
|
||||
ShelfPosition: b.ShelfPosition,
|
||||
AddedAt: b.AddedAt,
|
||||
LastSyncedAt: b.LastSyncedAt,
|
||||
Title: b.Title,
|
||||
Author: b.Author,
|
||||
FilePath: b.FilePath,
|
||||
MimeType: b.MimeType,
|
||||
EntitlementID: b.EntitlementID,
|
||||
KoboContentID: b.KoboContentID,
|
||||
RevisionNumber: b.RevisionNumber,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to fetch shelf"})
|
||||
}
|
||||
|
||||
type ShelfBookResponse struct {
|
||||
ID string `json:"id"`
|
||||
MediaItemID string `json:"media_item_id"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
FilePath string `json:"file_path"`
|
||||
MimeType string `json:"mime_type"`
|
||||
ShelfName string `json:"shelf_name"`
|
||||
ShelfPosition int `json:"shelf_position"`
|
||||
EntitlementID string `json:"entitlement_id,omitempty"`
|
||||
KoboContentID string `json:"kobo_content_id,omitempty"`
|
||||
RevisionNumber int `json:"revision_number"`
|
||||
AddedAt string `json:"added_at"`
|
||||
}
|
||||
|
||||
response := []ShelfBookResponse{}
|
||||
for _, book := range shelfBooks {
|
||||
response = append(response, ShelfBookResponse{
|
||||
ID: uuid.UUID(book.ID.Bytes).String(),
|
||||
MediaItemID: uuid.UUID(book.MediaItemID.Bytes).String(),
|
||||
Title: book.Title,
|
||||
Author: book.Author.String,
|
||||
FilePath: book.FilePath,
|
||||
MimeType: book.MimeType.String,
|
||||
ShelfName: book.ShelfName.String,
|
||||
ShelfPosition: int(book.ShelfPosition.Int32),
|
||||
EntitlementID: book.EntitlementID.String,
|
||||
KoboContentID: book.KoboContentID.String,
|
||||
RevisionNumber: int(book.RevisionNumber.Int32),
|
||||
AddedAt: book.AddedAt.Time.Format("2006-01-02T15:04:05Z07:00"),
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"status": "success",
|
||||
"device_id": deviceUUID.String(),
|
||||
"shelf_name": shelfName,
|
||||
"books": response,
|
||||
"total": len(response),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *MediaHandler) RemoveFromShelf(c echo.Context) error {
|
||||
deviceUUID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid device ID"})
|
||||
}
|
||||
|
||||
mediaUUID, err := uuid.Parse(c.QueryParam("media_item_id"))
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item ID"})
|
||||
}
|
||||
|
||||
pgDeviceUUID := pgtype.UUID{Bytes: deviceUUID, Valid: true}
|
||||
pgMediaUUID := pgtype.UUID{Bytes: mediaUUID, Valid: true}
|
||||
|
||||
err = h.db.RemoveBookFromKoboShelf(c.Request().Context(), database.RemoveBookFromKoboShelfParams{
|
||||
DeviceID: pgDeviceUUID,
|
||||
MediaItemID: pgMediaUUID,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to remove from shelf"})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"status": "success",
|
||||
"message": "book removed from shelf",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *MediaHandler) ClearShelf(c echo.Context) error {
|
||||
deviceUUID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid device ID"})
|
||||
}
|
||||
|
||||
shelfName := c.QueryParam("shelf")
|
||||
pgDeviceUUID := pgtype.UUID{Bytes: deviceUUID, Valid: true}
|
||||
|
||||
var result string
|
||||
if shelfName != "" {
|
||||
err = h.db.ClearKoboShelfByName(c.Request().Context(), database.ClearKoboShelfByNameParams{
|
||||
DeviceID: pgDeviceUUID,
|
||||
ShelfName: pgtype.Text{String: shelfName, Valid: true},
|
||||
})
|
||||
result = "shelf cleared"
|
||||
} else {
|
||||
err = h.db.ClearKoboShelf(c.Request().Context(), pgDeviceUUID)
|
||||
result = "all shelves cleared"
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to clear shelf"})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"status": "success",
|
||||
"message": result,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user