diff --git a/cmd/server/main.go b/cmd/server/main.go index d99ca16..24022c5 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -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") diff --git a/internal/handlers/kobo.go b/internal/handlers/kobo.go index f1e3c74..1284cd6 100644 --- a/internal/handlers/kobo.go +++ b/internal/handlers/kobo.go @@ -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, + }) +}