feat(handlers): integrate ProgressService into media, koreader, kobo, and queue
All four progress write paths now delegate to ProgressService.SaveProgress: - MediaHandler: UpdateMediaReadingProgress uses ProgressService for web saves with richer request body (reading_mode, zoom_level, scroll). GET now uses GetUniversalProgress query that JOINs media_items for format_group, total_characters, chapter_count. - KOReaderHandler: updateProgressForBook delegates to ProgressService. Fixed device ID bug (was using userID, now uses deviceID). Removed duplicate UpdateDeviceLastSync with zero UUID. Added pgtype helper functions (textPtrToPgText, intPtrToPgInt4, int64PtrToPgInt8). - KoboHandler: all four progress write points (Markup ReadingSync, Markup last-read-place, AnalyticsGettests, SyncFromServer) delegate to ProgressService. Fixed empty epubcfi string now correctly set to Valid: false. SyncFromServer preserves last_sync_source=bookhoard and Broadcast: false. - QueueProcessor: syncProgress delegates to ProgressService. - main.go: creates ProgressService after ConnectionManager, injects via SetProgressService() on all handlers and queue processor. Handler tests cover pgtype conversion helpers (textPtrToPgText, etc.) and device icon mapping.
This commit is contained in:
+96
-12
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"bookhoard/internal/database"
|
||||
"bookhoard/internal/services"
|
||||
wsync "bookhoard/internal/sync"
|
||||
"bookhoard/internal/utils"
|
||||
"context"
|
||||
"encoding/json"
|
||||
@@ -125,6 +126,7 @@ type MediaHandler struct {
|
||||
worker *services.Worker
|
||||
libraryService *services.LibraryService
|
||||
searchService *services.SearchService
|
||||
progressSvc *wsync.ProgressService
|
||||
}
|
||||
|
||||
func NewMediaHandler(db *database.Queries, libraryService *services.LibraryService, worker ...*services.Worker) *MediaHandler {
|
||||
@@ -139,6 +141,10 @@ func NewMediaHandler(db *database.Queries, libraryService *services.LibraryServi
|
||||
return mh
|
||||
}
|
||||
|
||||
func (mh *MediaHandler) SetProgressService(svc *wsync.ProgressService) {
|
||||
mh.progressSvc = svc
|
||||
}
|
||||
|
||||
func (h *MediaHandler) DownloadBook(c *echo.Context) error {
|
||||
bookUUID, err := uuid.Parse(c.Param("uuid"))
|
||||
if err != nil {
|
||||
@@ -890,7 +896,7 @@ func (mh *MediaHandler) GetMediaReadingProgress(c *echo.Context) error {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
|
||||
}
|
||||
|
||||
progress, err := mh.db.GetReadingProgress(c.Request().Context(), database.GetReadingProgressParams{
|
||||
progress, err := mh.db.GetUniversalProgress(c.Request().Context(), database.GetUniversalProgressParams{
|
||||
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
||||
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||
})
|
||||
@@ -904,7 +910,27 @@ func (mh *MediaHandler) GetMediaReadingProgress(c *echo.Context) error {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, progress)
|
||||
resp := map[string]interface{}{
|
||||
"id": progress.ID,
|
||||
"media_item_id": progress.MediaItemID,
|
||||
"user_id": progress.UserID,
|
||||
"current_page": progress.CurrentPage,
|
||||
"total_pages": progress.TotalPages,
|
||||
"last_read_at": progress.LastReadAt,
|
||||
"percentage": progress.Percentage,
|
||||
"character_offset": progress.CharacterOffset,
|
||||
"epubcfi": progress.Epubcfi,
|
||||
"chapter": progress.Chapter,
|
||||
"chapter_progress": progress.ChapterProgress,
|
||||
"format_group": progress.FormatGroup,
|
||||
"total_characters": progress.TotalCharacters,
|
||||
"chapter_count": progress.ChapterCount,
|
||||
"last_sync_device": progress.LastSyncDevice,
|
||||
"last_sync_source": progress.LastSyncSource,
|
||||
"last_sync_timestamp": progress.LastSyncTimestamp,
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// UpdateMediaReadingProgress handles PUT /api/media-items/:id/progress
|
||||
@@ -922,24 +948,82 @@ func (mh *MediaHandler) UpdateMediaReadingProgress(c *echo.Context) error {
|
||||
}
|
||||
|
||||
var req struct {
|
||||
CurrentPage int32 `json:"current_page"`
|
||||
TotalPages int32 `json:"total_pages"`
|
||||
Epubcfi string `json:"epubcfi"`
|
||||
Percentage float64 `json:"percentage"`
|
||||
CurrentPage *int32 `json:"current_page"`
|
||||
TotalPages *int32 `json:"total_pages"`
|
||||
Epubcfi *string `json:"epubcfi"`
|
||||
Percentage *float64 `json:"percentage"`
|
||||
Chapter *int `json:"chapter"`
|
||||
ChapterProgress *float64 `json:"chapter_progress"`
|
||||
CharacterOffset *int64 `json:"character_offset"`
|
||||
ReadingMode *string `json:"reading_mode"`
|
||||
ZoomLevel *float64 `json:"zoom_level"`
|
||||
ScrollX *float64 `json:"scroll_position_x"`
|
||||
ScrollY *float64 `json:"scroll_position_y"`
|
||||
}
|
||||
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()})
|
||||
|
||||
if mh.progressSvc != nil {
|
||||
saveReq := wsync.SaveProgressRequest{
|
||||
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
||||
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||
Source: "web",
|
||||
DeviceID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||
Percentage: req.Percentage,
|
||||
Epubcfi: req.Epubcfi,
|
||||
CharacterOffset: req.CharacterOffset,
|
||||
Chapter: req.Chapter,
|
||||
ChapterProgress: req.ChapterProgress,
|
||||
CurrentPage: nil,
|
||||
TotalPages: nil,
|
||||
ZoomLevel: req.ZoomLevel,
|
||||
ScrollX: req.ScrollX,
|
||||
ScrollY: req.ScrollY,
|
||||
ReadingMode: req.ReadingMode,
|
||||
DeviceType: "web",
|
||||
DeviceName: "Web",
|
||||
Broadcast: true,
|
||||
}
|
||||
if req.CurrentPage != nil {
|
||||
cp := int(*req.CurrentPage)
|
||||
saveReq.CurrentPage = &cp
|
||||
}
|
||||
if req.TotalPages != nil {
|
||||
tp := int(*req.TotalPages)
|
||||
saveReq.TotalPages = &tp
|
||||
}
|
||||
|
||||
result, err := mh.progressSvc.SaveProgress(c.Request().Context(), saveReq)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
percentage := 0.0
|
||||
if req.Percentage != nil {
|
||||
percentage = *req.Percentage
|
||||
}
|
||||
epubcfi := ""
|
||||
if req.Epubcfi != nil {
|
||||
epubcfi = *req.Epubcfi
|
||||
}
|
||||
currentPage := int32(0)
|
||||
if req.CurrentPage != nil {
|
||||
currentPage = *req.CurrentPage
|
||||
}
|
||||
totalPages := int32(0)
|
||||
if req.TotalPages != nil {
|
||||
totalPages = *req.TotalPages
|
||||
}
|
||||
|
||||
progress, err := mh.db.UpdateUniversalProgress(c.Request().Context(), database.UpdateUniversalProgressParams{
|
||||
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
||||
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||
Percentage: pgtype.Float8{Float64: req.Percentage, Valid: true},
|
||||
Percentage: pgtype.Float8{Float64: percentage, Valid: true},
|
||||
CharacterOffset: pgtype.Int8{Valid: false},
|
||||
Epubcfi: pgtype.Text{String: req.Epubcfi, Valid: req.Epubcfi != ""},
|
||||
Epubcfi: pgtype.Text{String: epubcfi, Valid: epubcfi != ""},
|
||||
Chapter: pgtype.Int4{Valid: false},
|
||||
ChapterProgress: pgtype.Float8{Valid: false},
|
||||
ViewportX: pgtype.Float8{Valid: false},
|
||||
@@ -951,8 +1035,8 @@ func (mh *MediaHandler) UpdateMediaReadingProgress(c *echo.Context) error {
|
||||
ReadingMode: pgtype.Text{Valid: false},
|
||||
LastSyncDevice: pgtype.Text{String: "web", Valid: true},
|
||||
LastSyncSource: pgtype.Text{String: "web", Valid: true},
|
||||
CurrentPage: pgtype.Int4{Int32: req.CurrentPage, Valid: true},
|
||||
TotalPages: pgtype.Int4{Int32: req.TotalPages, Valid: req.TotalPages > 0},
|
||||
CurrentPage: pgtype.Int4{Int32: currentPage, Valid: true},
|
||||
TotalPages: pgtype.Int4{Int32: totalPages, Valid: totalPages > 0},
|
||||
})
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
|
||||
Reference in New Issue
Block a user