feat: add server sync to Kobo endpoint and media routes

This commit is contained in:
2026-01-31 00:29:39 -05:00
parent fef41dc168
commit 42c3168fcf
2 changed files with 140 additions and 0 deletions
+9
View File
@@ -199,6 +199,15 @@ func main() {
koboSync.POST("/bookmark", deviceAuthMiddleware.Authenticate(koboHandler.Bookmark))
koboSync.POST("/v1/analytics/gettests", deviceAuthMiddleware.Authenticate(koboHandler.AnalyticsGettests))
koboSync.GET("/v1/initialization", deviceAuthMiddleware.Authenticate(koboHandler.Initialization))
koboSync.POST("/sync-from-server", deviceAuthMiddleware.Authenticate(koboHandler.SyncFromServer))
// Media item routes (download and shelf management)
mediaHandler := handlers.NewMediaHandler(queries)
e.GET("/api/books/:uuid/download", mediaHandler.DownloadBook)
protected.POST("/devices/:id/shelves", mediaHandler.AddToShelf)
protected.GET("/devices/:id/shelves", mediaHandler.GetShelf)
protected.DELETE("/devices/:id/shelves", mediaHandler.RemoveFromShelf)
protected.DELETE("/devices/:id/shelves/clear", mediaHandler.ClearShelf)
// Device management routes (protected - require user auth)
devices := protected.Group("/devices")
+131
View File
@@ -63,6 +63,10 @@ type KoboLibraryBook struct {
PagesRemaining *int `json:"PagesRemaining,omitempty"`
BookmarkCount int `json:"BookmarkCount"`
LastModified string `json:"LastModified"`
EntitlementId string `json:"EntitlementId,omitempty"`
Revision int `json:"Revision"`
MimeType string `json:"MimeType"`
FileSize int64 `json:"FileSize"`
}
type KoboLibraryResponse struct {
@@ -82,6 +86,20 @@ type KoboSyncStatus struct {
BookmarksSynced int `json:"BookmarksSynced"`
}
type KoboServerSyncData struct {
ContentId string `json:"ContentId"`
PercentRead float64 `json:"PercentRead"`
Bookmarks []KoboBookmarkSync `json:"Bookmarks,omitempty"`
Highlights []KoboBookmarkSync `json:"Highlights,omitempty"`
LastModified string `json:"LastModified"`
}
type KoboServerSyncResponse struct {
BooksSynced int `json:"BooksSynced"`
BookmarksSent int `json:"BookmarksSent"`
HighlightsSent int `json:"HighlightsSent"`
}
type KoboAnalyticsTest struct {
ContentId string `json:"ContentId"`
ReadingEvent string `json:"ReadingEvent"`
@@ -136,6 +154,27 @@ func (h *KoboHandler) Initialization(c echo.Context) error {
author = item.Author.String
}
entitlementId := item.EntitlementID.String
if entitlementId == "" || !item.EntitlementID.Valid {
entitlementId = "kobo_" + uuid.UUID(item.ID.Bytes).String()
}
mimeType := item.MimeType.String
if !item.MimeType.Valid {
mimeType = ""
}
fileSize := int64(0)
if item.FileSize.Valid {
fileSize = item.FileSize.Int64
}
revision := 1
if item.RevisionNumber.Valid {
revision = int(item.RevisionNumber.Int32)
}
librarySync = append(librarySync, KoboLibraryBook{
ContentId: uuid.UUID(item.ID.Bytes).String(),
ContentType: "6",
@@ -145,6 +184,10 @@ func (h *KoboHandler) Initialization(c echo.Context) error {
PagesRemaining: pagesRemaining,
BookmarkCount: bookmarkCount,
LastModified: lastModified,
EntitlementId: entitlementId,
Revision: revision,
MimeType: mimeType,
FileSize: fileSize,
})
}
@@ -389,3 +432,91 @@ func parseKoboDeviceHeader(c echo.Context) (KoboDeviceInfo, error) {
return device, nil
}
func (h *KoboHandler) SyncFromServer(c echo.Context) error {
device := c.Get("device").(database.Devices)
userID := device.UserID.Bytes
pgUserID := pgtype.UUID{Bytes: userID, Valid: true}
var req []KoboServerSyncData
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "invalid request format",
})
}
booksSynced := 0
bookmarksSent := 0
highlightsSent := 0
for _, syncData := range req {
mediaUUID, err := uuid.Parse(syncData.ContentId)
if err != nil {
continue
}
pgMediaUUID := pgtype.UUID{Bytes: mediaUUID, 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: "bookmann", Valid: true},
})
if err == nil {
booksSynced++
}
for _, bookmark := range syncData.Bookmarks {
if bookmark.BookmarkType == "bookmark" {
h.db.CreateMediaNote(c.Request().Context(), database.CreateMediaNoteParams{
MediaItemID: pgMediaUUID,
UserID: pgUserID,
Content: bookmark.BookmarkText,
Position: pgtype.Text{String: bookmark.BookmarkId, Valid: true},
})
bookmarksSent++
} else if bookmark.BookmarkType == "annotation" {
h.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
MediaItemID: pgMediaUUID,
UserID: pgUserID,
SelectionText: bookmark.BookmarkText,
StartPosition: pgtype.Text{String: bookmark.BookmarkId, Valid: true},
EndPosition: pgtype.Text{String: bookmark.BookmarkId, Valid: true},
Color: pgtype.Text{String: "#ffff00", Valid: true},
})
highlightsSent++
}
}
for _, highlight := range syncData.Highlights {
h.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
MediaItemID: pgMediaUUID,
UserID: pgUserID,
SelectionText: highlight.BookmarkText,
StartPosition: pgtype.Text{String: highlight.BookmarkId, Valid: true},
EndPosition: pgtype.Text{String: highlight.BookmarkId, Valid: true},
Color: pgtype.Text{String: "#ffff00", Valid: true},
})
highlightsSent++
}
}
_, err := h.db.UpdateDeviceLastSync(c.Request().Context(), device.ID)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "failed to update device timestamp",
})
}
return c.JSON(http.StatusOK, KoboServerSyncResponse{
BooksSynced: booksSynced,
BookmarksSent: bookmarksSent,
HighlightsSent: highlightsSent,
})
}