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:
+59
-134
@@ -3,14 +3,11 @@ package handlers
|
||||
import (
|
||||
"bookhoard/internal/database"
|
||||
wsync "bookhoard/internal/sync"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/labstack/echo/v5"
|
||||
)
|
||||
@@ -19,12 +16,17 @@ type KOReaderHandler struct {
|
||||
db *database.Queries
|
||||
connManager *wsync.ConnectionManager
|
||||
queue *wsync.SyncQueueProcessor
|
||||
progressSvc *wsync.ProgressService
|
||||
}
|
||||
|
||||
func NewKOReaderHandler(db *database.Queries, connManager *wsync.ConnectionManager, queue *wsync.SyncQueueProcessor) *KOReaderHandler {
|
||||
return &KOReaderHandler{db: db, connManager: connManager, queue: queue}
|
||||
}
|
||||
|
||||
func (h *KOReaderHandler) SetProgressService(svc *wsync.ProgressService) {
|
||||
h.progressSvc = svc
|
||||
}
|
||||
|
||||
type KOReaderProgressRequest struct {
|
||||
LibraryID *string `json:"library_id,omitempty"`
|
||||
Books []KOReaderBookProgress `json:"books" validate:"required"`
|
||||
@@ -192,7 +194,7 @@ func (h *KOReaderHandler) SyncProgress(c *echo.Context) error {
|
||||
continue
|
||||
}
|
||||
|
||||
err := h.updateProgressForBook(c, pgUserID, mediaItemID, book)
|
||||
err := h.updateProgressForBook(c, device.ID, pgUserID, mediaItemID, book)
|
||||
if err == nil {
|
||||
booksSynced++
|
||||
}
|
||||
@@ -394,68 +396,46 @@ func (h *KOReaderHandler) enqueueProgressForBook(c *echo.Context, deviceID pgtyp
|
||||
return h.queue.EnqueueProgress(update)
|
||||
}
|
||||
|
||||
func (h *KOReaderHandler) updateProgressForBook(c *echo.Context, userID pgtype.UUID, mediaItemID pgtype.UUID, book KOReaderBookProgress) error {
|
||||
func (h *KOReaderHandler) updateProgressForBook(c *echo.Context, deviceID pgtype.UUID, userID pgtype.UUID, mediaItemID pgtype.UUID, book KOReaderBookProgress) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
existingProgress, err := h.db.GetReadingProgress(ctx, database.GetReadingProgressParams{
|
||||
MediaItemID: mediaItemID,
|
||||
UserID: userID,
|
||||
})
|
||||
deviceInfo := book.DeviceInfo
|
||||
deviceModel := deviceInfo.DeviceModel
|
||||
if deviceModel == "" {
|
||||
deviceModel = "KOReader Device"
|
||||
}
|
||||
|
||||
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
||||
if h.progressSvc != nil {
|
||||
saveReq := wsync.SaveProgressRequest{
|
||||
MediaItemID: mediaItemID,
|
||||
UserID: userID,
|
||||
Source: "koreader",
|
||||
DeviceID: deviceID,
|
||||
Percentage: &book.Percentage,
|
||||
Epubcfi: book.Epubcfi,
|
||||
Chapter: book.Chapter,
|
||||
CharacterOffset: book.Character,
|
||||
CurrentPage: book.Page,
|
||||
TotalPages: book.TotalPages,
|
||||
DeviceType: "koreader",
|
||||
DeviceName: deviceModel,
|
||||
Broadcast: true,
|
||||
}
|
||||
|
||||
_, err := h.progressSvc.SaveProgress(ctx, saveReq)
|
||||
return err
|
||||
}
|
||||
|
||||
hasExistingProgress := !errors.Is(err, pgx.ErrNoRows)
|
||||
conflictDetected := false
|
||||
|
||||
if hasExistingProgress && existingProgress.LastSyncSource.Valid {
|
||||
if existingProgress.LastSyncSource.String != "koreader" && existingProgress.LastSyncTimestamp.Valid {
|
||||
timeDiff := time.Since(existingProgress.LastSyncTimestamp.Time)
|
||||
if timeDiff < 5*time.Minute {
|
||||
percentageDiff := book.Percentage - existingProgress.Percentage.Float64
|
||||
if percentageDiff < 0 {
|
||||
percentageDiff = -percentageDiff
|
||||
}
|
||||
if percentageDiff > 0.01 {
|
||||
conflictDetected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var epubcfi pgtype.Text
|
||||
var chapter pgtype.Int4
|
||||
var characterOffset pgtype.Int8
|
||||
var currentPage pgtype.Int4
|
||||
var totalPages pgtype.Int4
|
||||
|
||||
if book.Epubcfi != nil {
|
||||
epubcfi = pgtype.Text{String: *book.Epubcfi, Valid: true}
|
||||
}
|
||||
if book.Chapter != nil {
|
||||
chapter = pgtype.Int4{Int32: int32(*book.Chapter), Valid: true}
|
||||
}
|
||||
if book.Character != nil {
|
||||
characterOffset = pgtype.Int8{Int64: *book.Character, Valid: true}
|
||||
}
|
||||
if book.Page != nil {
|
||||
currentPage = pgtype.Int4{Int32: int32(*book.Page), Valid: true}
|
||||
}
|
||||
if book.TotalPages != nil {
|
||||
totalPages = pgtype.Int4{Int32: int32(*book.TotalPages), Valid: true}
|
||||
}
|
||||
|
||||
_, err = h.db.UpdateUniversalProgress(ctx, database.UpdateUniversalProgressParams{
|
||||
_, err := h.db.UpdateUniversalProgress(ctx, database.UpdateUniversalProgressParams{
|
||||
MediaItemID: mediaItemID,
|
||||
UserID: userID,
|
||||
Percentage: pgtype.Float8{Float64: book.Percentage, Valid: true},
|
||||
Epubcfi: epubcfi,
|
||||
Chapter: chapter,
|
||||
Epubcfi: textPtrToPgText(book.Epubcfi),
|
||||
Chapter: intPtrToPgInt4(book.Chapter),
|
||||
ChapterProgress: pgtype.Float8{Float64: book.Percentage, Valid: true},
|
||||
CharacterOffset: characterOffset,
|
||||
CurrentPage: currentPage,
|
||||
TotalPages: totalPages,
|
||||
CharacterOffset: int64PtrToPgInt8(book.Character),
|
||||
CurrentPage: intPtrToPgInt4(book.Page),
|
||||
TotalPages: intPtrToPgInt4(book.TotalPages),
|
||||
LastSyncDevice: pgtype.Text{String: "koreader", Valid: true},
|
||||
LastSyncSource: pgtype.Text{String: "koreader", Valid: true},
|
||||
ViewportY: pgtype.Float8{},
|
||||
@@ -465,97 +445,42 @@ func (h *KOReaderHandler) updateProgressForBook(c *echo.Context, userID pgtype.U
|
||||
ReadingMode: pgtype.Text{},
|
||||
ZoomLevel: pgtype.Float8{},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if conflictDetected {
|
||||
koreaderData := map[string]interface{}{
|
||||
"source": "koreader",
|
||||
"timestamp": time.Now(),
|
||||
"data": map[string]interface{}{
|
||||
"percentage": book.Percentage,
|
||||
},
|
||||
}
|
||||
if book.Epubcfi != nil {
|
||||
koreaderData["data"].(map[string]interface{})["epubcfi"] = *book.Epubcfi
|
||||
}
|
||||
if book.Chapter != nil {
|
||||
koreaderData["data"].(map[string]interface{})["chapter"] = *book.Chapter
|
||||
}
|
||||
if book.Character != nil {
|
||||
koreaderData["data"].(map[string]interface{})["character"] = *book.Character
|
||||
}
|
||||
if book.Page != nil {
|
||||
koreaderData["data"].(map[string]interface{})["page"] = *book.Page
|
||||
}
|
||||
if book.TotalPages != nil {
|
||||
koreaderData["data"].(map[string]interface{})["total_pages"] = *book.TotalPages
|
||||
}
|
||||
|
||||
existingData := map[string]interface{}{
|
||||
"source": existingProgress.LastSyncSource.String,
|
||||
"timestamp": existingProgress.LastSyncTimestamp.Time,
|
||||
"data": map[string]interface{}{
|
||||
"percentage": existingProgress.Percentage.Float64,
|
||||
},
|
||||
}
|
||||
if existingProgress.Epubcfi.Valid {
|
||||
existingData["data"].(map[string]interface{})["epubcfi"] = existingProgress.Epubcfi.String
|
||||
}
|
||||
if existingProgress.Chapter.Valid {
|
||||
existingData["data"].(map[string]interface{})["chapter"] = existingProgress.Chapter.Int32
|
||||
}
|
||||
if existingProgress.CharacterOffset.Valid {
|
||||
existingData["data"].(map[string]interface{})["character"] = existingProgress.CharacterOffset.Int64
|
||||
}
|
||||
if existingProgress.CurrentPage.Valid {
|
||||
existingData["data"].(map[string]interface{})["page"] = existingProgress.CurrentPage.Int32
|
||||
}
|
||||
if existingProgress.TotalPages.Valid {
|
||||
existingData["data"].(map[string]interface{})["total_pages"] = existingProgress.TotalPages.Int32
|
||||
}
|
||||
|
||||
conflictData := map[string]interface{}{
|
||||
"koreader": koreaderData,
|
||||
"existing": existingData,
|
||||
}
|
||||
conflictDataJSON, _ := json.Marshal(conflictData)
|
||||
|
||||
_, err := h.db.CreateSyncConflict(ctx, database.CreateSyncConflictParams{
|
||||
MediaItemID: mediaItemID,
|
||||
UserID: userID,
|
||||
ConflictType: "progress",
|
||||
ConflictData: conflictDataJSON,
|
||||
})
|
||||
if err == nil {
|
||||
h.connManager.BroadcastConflictNotification(
|
||||
mediaItemID.Bytes,
|
||||
"detection",
|
||||
"",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
deviceInfo := book.DeviceInfo
|
||||
if deviceInfo.DeviceModel == "" {
|
||||
deviceInfo.DeviceModel = "KOReader Device"
|
||||
}
|
||||
|
||||
h.connManager.BroadcastProgressUpdate(
|
||||
mediaItemID.Bytes,
|
||||
book.Percentage,
|
||||
wsync.SourceDevice{
|
||||
ID: uuid.UUID(userID.Bytes).String(),
|
||||
Name: deviceInfo.DeviceModel,
|
||||
ID: uuid.UUID(deviceID.Bytes).String(),
|
||||
Name: deviceModel,
|
||||
Type: "koreader",
|
||||
},
|
||||
)
|
||||
|
||||
_, err = h.db.UpdateDeviceLastSync(ctx, pgtype.UUID{Bytes: [16]byte{}, Valid: false})
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
func textPtrToPgText(s *string) pgtype.Text {
|
||||
if s != nil {
|
||||
return pgtype.Text{String: *s, Valid: true}
|
||||
}
|
||||
return pgtype.Text{}
|
||||
}
|
||||
|
||||
func intPtrToPgInt4(i *int) pgtype.Int4 {
|
||||
if i != nil {
|
||||
return pgtype.Int4{Int32: int32(*i), Valid: true}
|
||||
}
|
||||
return pgtype.Int4{}
|
||||
}
|
||||
|
||||
func int64PtrToPgInt8(i *int64) pgtype.Int8 {
|
||||
if i != nil {
|
||||
return pgtype.Int8{Int64: *i, Valid: true}
|
||||
}
|
||||
return pgtype.Int8{}
|
||||
}
|
||||
|
||||
func (h *KOReaderHandler) GetMetadata(c *echo.Context) error {
|
||||
|
||||
Reference in New Issue
Block a user