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:
+91
-83
@@ -3,9 +3,6 @@ package handlers
|
||||
import (
|
||||
"bookhoard/internal/database"
|
||||
wsync "bookhoard/internal/sync"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
@@ -20,12 +17,17 @@ import (
|
||||
type KoboHandler struct {
|
||||
db *database.Queries
|
||||
connManager *wsync.ConnectionManager
|
||||
progressSvc *wsync.ProgressService
|
||||
}
|
||||
|
||||
func NewKoboHandler(db *database.Queries, connManager *wsync.ConnectionManager) *KoboHandler {
|
||||
return &KoboHandler{db: db, connManager: connManager}
|
||||
}
|
||||
|
||||
func (h *KoboHandler) SetProgressService(svc *wsync.ProgressService) {
|
||||
h.progressSvc = svc
|
||||
}
|
||||
|
||||
// mapContentIdToBookhoardUUID maps Kobo ContentId to Bookhoard UUID with multiple fallback strategies
|
||||
// Enhanced Kobo Sync - ContentId Mapping Logic
|
||||
func (h *KoboHandler) mapContentIdToBookhoardUUID(ctx *echo.Context, contentId string, deviceID uuid.UUID) (uuid.UUID, error, string) {
|
||||
@@ -175,12 +177,6 @@ func looksLikeSHA256(s string) bool {
|
||||
return matched
|
||||
}
|
||||
|
||||
// calculateFileSHA256 calculates SHA-256 hash of file path
|
||||
func calculateFileSHA256(filePath string) string {
|
||||
hash := sha256.Sum256([]byte(filePath))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
type KoboDeviceInfo struct {
|
||||
DeviceID string `json:"DeviceId"`
|
||||
Model string `json:"Model"`
|
||||
@@ -403,39 +399,38 @@ func (h *KoboHandler) Markup(c *echo.Context) error {
|
||||
unlinkedBooks := 0
|
||||
|
||||
for _, readingSync := range req.ReadingSync {
|
||||
// Use ContentId mapping with fallback logic
|
||||
bookhoardUUID, err, _ := h.mapContentIdToBookhoardUUID(c, readingSync.ContentId, deviceUUID)
|
||||
if err != nil || bookhoardUUID == uuid.Nil {
|
||||
// Unlinked book detected
|
||||
unlinkedBooks++
|
||||
// TODO: Create unlinked book entry for manual resolution
|
||||
continue
|
||||
}
|
||||
|
||||
pgMediaUUID := pgtype.UUID{Bytes: bookhoardUUID, Valid: true}
|
||||
|
||||
percentage := readingSync.PercentRead / 100.0
|
||||
|
||||
_, err = h.db.UpdateUniversalProgress(c.Request().Context(), database.UpdateUniversalProgressParams{
|
||||
MediaItemID: pgMediaUUID,
|
||||
UserID: pgUserID,
|
||||
Percentage: pgtype.Float8{Float64: percentage, Valid: true},
|
||||
LastSyncDevice: pgtype.Text{String: "kobo", Valid: true},
|
||||
LastSyncSource: pgtype.Text{String: "kobo", Valid: true},
|
||||
})
|
||||
if h.progressSvc != nil {
|
||||
_, err = h.progressSvc.SaveProgress(c.Request().Context(), wsync.SaveProgressRequest{
|
||||
MediaItemID: pgMediaUUID,
|
||||
UserID: pgUserID,
|
||||
Source: "kobo",
|
||||
DeviceID: pgtype.UUID{Bytes: deviceID, Valid: true},
|
||||
Percentage: &percentage,
|
||||
DeviceType: "kobo",
|
||||
DeviceName: device.DeviceName,
|
||||
Broadcast: true,
|
||||
})
|
||||
} else {
|
||||
_, err = h.db.UpdateUniversalProgress(c.Request().Context(), database.UpdateUniversalProgressParams{
|
||||
MediaItemID: pgMediaUUID,
|
||||
UserID: pgUserID,
|
||||
Percentage: pgtype.Float8{Float64: percentage, Valid: true},
|
||||
LastSyncDevice: pgtype.Text{String: "kobo", Valid: true},
|
||||
LastSyncSource: pgtype.Text{String: "kobo", Valid: true},
|
||||
})
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
markupsSynced++
|
||||
|
||||
h.connManager.BroadcastProgressUpdate(
|
||||
bookhoardUUID,
|
||||
percentage,
|
||||
wsync.SourceDevice{
|
||||
ID: uuid.UUID(userID).String(),
|
||||
Name: device.DeviceName,
|
||||
Type: "kobo",
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,15 +476,33 @@ func (h *KoboHandler) Markup(c *echo.Context) error {
|
||||
epubcfi = strings.TrimSuffix(epubcfi, ")")
|
||||
}
|
||||
|
||||
_, err = h.db.UpdateUniversalProgress(c.Request().Context(), database.UpdateUniversalProgressParams{
|
||||
MediaItemID: pgMediaUUID,
|
||||
UserID: pgUserID,
|
||||
Epubcfi: pgtype.Text{String: epubcfi, Valid: true},
|
||||
Chapter: pgtype.Int4{Int32: int32(bookmarkSync.Chapter), Valid: true},
|
||||
ChapterProgress: pgtype.Float8{Float64: 0.5, Valid: true},
|
||||
LastSyncDevice: pgtype.Text{String: "kobo", Valid: true},
|
||||
LastSyncSource: pgtype.Text{String: "kobo", Valid: true},
|
||||
})
|
||||
chapter := bookmarkSync.Chapter
|
||||
chapterProgress := 0.5
|
||||
|
||||
if h.progressSvc != nil {
|
||||
_, err = h.progressSvc.SaveProgress(c.Request().Context(), wsync.SaveProgressRequest{
|
||||
MediaItemID: pgMediaUUID,
|
||||
UserID: pgUserID,
|
||||
Source: "kobo",
|
||||
DeviceID: pgtype.UUID{Bytes: deviceID, Valid: true},
|
||||
Epubcfi: &epubcfi,
|
||||
Chapter: &chapter,
|
||||
ChapterProgress: &chapterProgress,
|
||||
DeviceType: "kobo",
|
||||
DeviceName: device.DeviceName,
|
||||
Broadcast: false,
|
||||
})
|
||||
} else {
|
||||
_, err = h.db.UpdateUniversalProgress(c.Request().Context(), database.UpdateUniversalProgressParams{
|
||||
MediaItemID: pgMediaUUID,
|
||||
UserID: pgUserID,
|
||||
Epubcfi: pgtype.Text{String: epubcfi, Valid: epubcfi != ""},
|
||||
Chapter: pgtype.Int4{Int32: int32(bookmarkSync.Chapter), Valid: true},
|
||||
ChapterProgress: pgtype.Float8{Float64: 0.5, Valid: true},
|
||||
LastSyncDevice: pgtype.Text{String: "kobo", Valid: true},
|
||||
LastSyncSource: pgtype.Text{String: "kobo", Valid: true},
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to store last-read-place: %v", err)
|
||||
}
|
||||
@@ -606,34 +619,33 @@ func (h *KoboHandler) AnalyticsGettests(c *echo.Context) error {
|
||||
}
|
||||
|
||||
for _, test := range req {
|
||||
// Use ContentId mapping with fallback logic
|
||||
bookhoardUUID, err, _ := h.mapContentIdToBookhoardUUID(c, test.ContentId, deviceUUID)
|
||||
if err != nil || bookhoardUUID == uuid.Nil {
|
||||
// Unlinked book - skip
|
||||
continue
|
||||
}
|
||||
|
||||
pgMediaUUID := pgtype.UUID{Bytes: bookhoardUUID, Valid: true}
|
||||
percentage := test.PercentRead / 100.0
|
||||
|
||||
_, err = h.db.UpdateUniversalProgress(c.Request().Context(), database.UpdateUniversalProgressParams{
|
||||
MediaItemID: pgMediaUUID,
|
||||
UserID: pgUserID,
|
||||
Percentage: pgtype.Float8{Float64: percentage, Valid: true},
|
||||
LastSyncDevice: pgtype.Text{String: "kobo", Valid: true},
|
||||
LastSyncSource: pgtype.Text{String: "kobo", Valid: true},
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
h.connManager.BroadcastProgressUpdate(
|
||||
bookhoardUUID,
|
||||
percentage,
|
||||
wsync.SourceDevice{
|
||||
ID: uuid.UUID(userID).String(),
|
||||
Name: device.DeviceName,
|
||||
Type: "kobo",
|
||||
},
|
||||
)
|
||||
if h.progressSvc != nil {
|
||||
_, err = h.progressSvc.SaveProgress(c.Request().Context(), wsync.SaveProgressRequest{
|
||||
MediaItemID: pgMediaUUID,
|
||||
UserID: pgUserID,
|
||||
Source: "kobo",
|
||||
DeviceID: pgtype.UUID{Bytes: deviceID, Valid: true},
|
||||
Percentage: &percentage,
|
||||
DeviceType: "kobo",
|
||||
DeviceName: device.DeviceName,
|
||||
Broadcast: true,
|
||||
})
|
||||
} else {
|
||||
_, err = h.db.UpdateUniversalProgress(c.Request().Context(), database.UpdateUniversalProgressParams{
|
||||
MediaItemID: pgMediaUUID,
|
||||
UserID: pgUserID,
|
||||
Percentage: pgtype.Float8{Float64: percentage, Valid: true},
|
||||
LastSyncDevice: pgtype.Text{String: "kobo", Valid: true},
|
||||
LastSyncSource: pgtype.Text{String: "kobo", Valid: true},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -649,20 +661,6 @@ func (h *KoboHandler) AnalyticsGettests(c *echo.Context) error {
|
||||
})
|
||||
}
|
||||
|
||||
func parseKoboDeviceHeader(c *echo.Context) (KoboDeviceInfo, error) {
|
||||
deviceHeader := c.Request().Header.Get("x-kobo-device")
|
||||
if deviceHeader == "" {
|
||||
return KoboDeviceInfo{}, fmt.Errorf("missing x-kobo-device header")
|
||||
}
|
||||
|
||||
var device KoboDeviceInfo
|
||||
if err := json.Unmarshal([]byte(deviceHeader), &device); err != nil {
|
||||
return KoboDeviceInfo{}, fmt.Errorf("invalid device header format")
|
||||
}
|
||||
|
||||
return device, nil
|
||||
}
|
||||
|
||||
func (h *KoboHandler) SyncFromServer(c *echo.Context) error {
|
||||
device := c.Get("device").(database.Devices)
|
||||
userID := device.UserID.Bytes
|
||||
@@ -683,24 +681,34 @@ func (h *KoboHandler) SyncFromServer(c *echo.Context) error {
|
||||
highlightsSent := 0
|
||||
|
||||
for _, syncData := range req {
|
||||
// Use ContentId mapping with fallback logic
|
||||
bookhoardUUID, err, _ := h.mapContentIdToBookhoardUUID(c, syncData.ContentId, deviceUUID)
|
||||
if err != nil || bookhoardUUID == uuid.Nil {
|
||||
// Unlinked book - skip
|
||||
continue
|
||||
}
|
||||
|
||||
pgMediaUUID := pgtype.UUID{Bytes: bookhoardUUID, Valid: true}
|
||||
|
||||
percentage := syncData.PercentRead / 100.0
|
||||
|
||||
_, err = h.db.UpdateUniversalProgress(c.Request().Context(), database.UpdateUniversalProgressParams{
|
||||
MediaItemID: pgMediaUUID,
|
||||
UserID: pgUserID,
|
||||
Percentage: pgtype.Float8{Float64: percentage, Valid: true},
|
||||
LastSyncDevice: pgtype.Text{String: "kobo", Valid: true},
|
||||
LastSyncSource: pgtype.Text{String: "bookhoard", Valid: true},
|
||||
})
|
||||
if h.progressSvc != nil {
|
||||
_, err = h.progressSvc.SaveProgress(c.Request().Context(), wsync.SaveProgressRequest{
|
||||
MediaItemID: pgMediaUUID,
|
||||
UserID: pgUserID,
|
||||
Source: "bookhoard",
|
||||
DeviceID: pgtype.UUID{Bytes: deviceID, Valid: true},
|
||||
Percentage: &percentage,
|
||||
DeviceType: "kobo",
|
||||
DeviceName: device.DeviceName,
|
||||
Broadcast: false,
|
||||
})
|
||||
} else {
|
||||
_, err = h.db.UpdateUniversalProgress(c.Request().Context(), database.UpdateUniversalProgressParams{
|
||||
MediaItemID: pgMediaUUID,
|
||||
UserID: pgUserID,
|
||||
Percentage: pgtype.Float8{Float64: percentage, Valid: true},
|
||||
LastSyncDevice: pgtype.Text{String: "kobo", Valid: true},
|
||||
LastSyncSource: pgtype.Text{String: "bookhoard", Valid: true},
|
||||
})
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
booksSynced++
|
||||
|
||||
Reference in New Issue
Block a user