Batch H1 step 4: - Handler GetUniversalProgress (progress.go) deleted — no route mounts it; only /api/progress/:id/history (GetProgressHistory) and the SSR /progress page exist. Note: the bruno universal-progress suite documents GET/PUT /api/progress/:id + /api/progress/device/:device_id — those routes do NOT exist server-side; that suite is stale documentation, flagged in the plan. - Handler GetAllProgress deleted — unregistered near-copy of GetAllProgressData (which the /progress SSR page uses and stays). - series.go: dropped the `_ = libTypes` no-op (libTypes IS used by the Badge line right below — the assignment stays). - router/helpers.go: visibleLibraryIDsFor's signature and first statement were fused onto one line; split for readability. No behavior change.
177 lines
5.3 KiB
Go
177 lines
5.3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bookhoard/internal/database"
|
|
wsync "bookhoard/internal/sync"
|
|
"bookhoard/internal/utils"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/labstack/echo/v5"
|
|
)
|
|
|
|
// getDeviceIcon returns an emoji icon for device type
|
|
func getDeviceIcon(deviceType string) string {
|
|
switch deviceType {
|
|
case "kobo":
|
|
return "📚"
|
|
case "koreader":
|
|
return "📖"
|
|
case "kindle":
|
|
return "📱"
|
|
default:
|
|
return "📚"
|
|
}
|
|
}
|
|
|
|
// UpdateUniversalProgress updates progress with automatic conversion
|
|
// GetProgressHistory retrieves reading session history
|
|
func (h *Handler) GetProgressHistory(c *echo.Context) error {
|
|
user := MustGetAuthenticatedUser(c)
|
|
|
|
mediaItemID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "invalid media item ID")
|
|
}
|
|
|
|
limit := 50
|
|
if l := c.QueryParam("limit"); l != "" {
|
|
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 100 {
|
|
limit = parsed
|
|
}
|
|
}
|
|
|
|
history, err := h.db.GetReadingHistory(context.Background(), database.GetReadingHistoryParams{
|
|
UserID: pgtype.UUID{Bytes: user.ID.Bytes, Valid: true},
|
|
MediaItemID: pgtype.UUID{Bytes: mediaItemID, Valid: true},
|
|
Limit: int32(limit),
|
|
})
|
|
if err != nil {
|
|
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get history")
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"media_item_id": mediaItemID,
|
|
"sessions": history,
|
|
"count": len(history),
|
|
})
|
|
}
|
|
|
|
type ProgressWithMedia struct {
|
|
MediaItemID uuid.UUID `json:"media_item_id"`
|
|
Title string `json:"title"`
|
|
Author string `json:"author"`
|
|
CoverImagePath string `json:"cover_image_path"`
|
|
Percentage float64 `json:"percentage"`
|
|
CurrentPage int32 `json:"current_page"`
|
|
TotalPages int32 `json:"total_pages"`
|
|
LastReadAt time.Time `json:"last_read_at"`
|
|
Epubcfi string `json:"epubcfi"`
|
|
LastSyncDevice string `json:"last_sync_device"`
|
|
DeviceName string `json:"device_name,omitempty"`
|
|
DeviceType string `json:"device_type,omitempty"`
|
|
DeviceIcon string `json:"device_icon,omitempty"`
|
|
ProgressPercentage float64 `json:"-"`
|
|
EpubCFI string `json:"-"`
|
|
LastUpdated string `json:"-"`
|
|
FormatGroup string `json:"format_group"`
|
|
EstimatedPages int `json:"estimated_pages"`
|
|
// Audiobook whole-book duration (seconds) for H:M:S progress display
|
|
DurationSeconds float64 `json:"duration_seconds,omitempty"`
|
|
}
|
|
|
|
// audioDurationFrom pulls duration_seconds from the scan's audio_metadata
|
|
// JSONB (0 when the item isn't audio or hasn't been probed).
|
|
func audioDurationFrom(raw []byte) float64 {
|
|
if len(raw) == 0 {
|
|
return 0
|
|
}
|
|
var payload struct {
|
|
DurationSeconds float64 `json:"duration_seconds"`
|
|
}
|
|
if json.Unmarshal(raw, &payload) != nil {
|
|
return 0
|
|
}
|
|
return payload.DurationSeconds
|
|
}
|
|
|
|
// GetAllProgressData retrieves all progress for a user with sync source info.
|
|
// The registered /progress SSR page renders this (frontend.go); the old
|
|
// GetAllProgress HTTP handler (an unregistered near-copy) was deleted.
|
|
func (h *Handler) GetAllProgressData(c *echo.Context) ([]ProgressWithMedia, error) {
|
|
user := MustGetAuthenticatedUser(c)
|
|
|
|
progressList := []ProgressWithMedia{}
|
|
|
|
mediaItems, err := h.db.ListMediaItems(c.Request().Context(), database.ListMediaItemsParams{
|
|
Limit: 1000,
|
|
Offset: 0,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, mediaItem := range mediaItems {
|
|
progress, err := h.db.GetUniversalProgress(c.Request().Context(), database.GetUniversalProgressParams{
|
|
MediaItemID: mediaItem.ID,
|
|
UserID: user.ID,
|
|
})
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
coverPath := utils.ResolveMediaURL(mediaItem.LibraryID, mediaItem.CoverImagePath)
|
|
|
|
author := ""
|
|
if mediaItem.Author.Valid {
|
|
author = mediaItem.Author.String
|
|
}
|
|
|
|
epubcfi := ""
|
|
if progress.Epubcfi.Valid {
|
|
epubcfi = progress.Epubcfi.String
|
|
}
|
|
|
|
deviceName := ""
|
|
if progress.LastSyncDevice.Valid {
|
|
deviceName = progress.LastSyncDevice.String
|
|
}
|
|
|
|
lastUpdated := ""
|
|
if progress.LastReadAt.Valid {
|
|
lastUpdated = utils.FormatTimeInTimezone(progress.LastReadAt.Time, user.Timezone.String)
|
|
}
|
|
|
|
progressList = append(progressList, ProgressWithMedia{
|
|
MediaItemID: progress.MediaItemID.Bytes,
|
|
Title: mediaItem.Title,
|
|
Author: author,
|
|
CoverImagePath: coverPath,
|
|
Percentage: progress.Percentage.Float64,
|
|
CurrentPage: progress.CurrentPage.Int32,
|
|
TotalPages: progress.TotalPages.Int32,
|
|
LastReadAt: progress.LastReadAt.Time,
|
|
Epubcfi: epubcfi,
|
|
LastSyncDevice: deviceName,
|
|
ProgressPercentage: progress.Percentage.Float64 * 100,
|
|
EpubCFI: epubcfi,
|
|
LastUpdated: lastUpdated,
|
|
DeviceIcon: getDeviceIcon(deviceName),
|
|
DeviceName: deviceName,
|
|
DeviceType: deviceName,
|
|
FormatGroup: mediaItem.FormatGroup,
|
|
// Audiobook "current position" needs whole-book duration
|
|
// (H:M:S display); parsed from the scanner's audio_metadata.
|
|
DurationSeconds: audioDurationFrom(mediaItem.AudioMetadata),
|
|
EstimatedPages: wsync.EstimatedPages(mediaItem.TotalCharacters.Int64),
|
|
})
|
|
}
|
|
|
|
return progressList, nil
|
|
}
|