feat(sync): wire annotation sync into all device and web handlers
Complete the annotation sync pipeline across all ingest and serve paths.
Previously, annotations sent inline with KOReader progress pushes were
silently discarded, and no annotations were ever served back to devices.
INGEST (device → server):
KOReader (koreader.go):
- Add processBookAnnotations helper that processes inline highlights,
notes, and bookmarks from every progress push (immediate + checkpoint)
- Highlights get CRE→CFI position conversion before SaveHighlight
- KOReader 'notes' (text + notes) stored as highlights with NoteText
to ensure correct round-trip classification
- Bookmarks routed through SaveBookmark with device sync data
- Called from both updateProgressForBook and handleCheckpointSync
Kobo (kobo.go):
- Markup handler: annotations and bookmarks route through
AnnotationService (SaveHighlight/SaveBookmark)
- Bookmark handler: same routing with device sync data
- SyncFromServer handler: same routing
- All handlers fall back to direct DB calls when annotationSvc == nil
Web reader (media.go):
- CreateMediaHighlight → SaveHighlight (Source="web", ModifiedAt=now)
- CreateMediaNote → SaveNote (Source="web")
- DeleteMediaHighlight → TombstoneHighlightByID
- DeleteMediaNote → TombstoneNoteByID (was hard delete, now tombstone)
- All fall back to old behavior when annotationSvc == nil
SERVE (server → device):
KOReader GetMetadata (koreader.go):
- Query and serve bookmarks from media_bookmarks table (was missing)
- Serve deleted_highlights and deleted_bookmarks arrays containing
device_sync_data + dedup_key for client-side deletion
- Highlights/notes already served with reverse CFI conversion
Kobo Markup handler (kobo.go):
- Track processed books during sync
- Query tombstones per book, extract bookmark_id from device_sync_data
- Return DeletedAnnotations array in KoboSyncStatus response
Conflict resolution (conflicts.go):
- Enable annotation conflict types in ResolveConflict handler
- Add applyAnnotationResolution dispatching to:
applyHighlightResolution / applyBookmarkResolution / applyNoteResolution
- Each looks up by dedup_key and applies winner's fields
- Allow manual override of auto_resolved conflicts
(changed check from != "unresolved" to == "user_resolved")
Infrastructure:
- AnnotationService field + SetAnnotationService in router Config
- Inject AnnotationService into KOReader, Kobo, Media handlers
- Start tombstone purger goroutine in main.go (24h interval)
- Test helpers: construct AnnotationService in test setup
This commit is contained in:
@@ -63,9 +63,13 @@ func main() {
|
|||||||
connManager := sync.NewConnectionManager()
|
connManager := sync.NewConnectionManager()
|
||||||
|
|
||||||
progressService := sync.NewProgressService(queries, connManager)
|
progressService := sync.NewProgressService(queries, connManager)
|
||||||
|
annotationService := sync.NewAnnotationService(queries, connManager)
|
||||||
|
tombstonePurgerCancel := annotationService.StartTombstonePurger()
|
||||||
|
defer tombstonePurgerCancel()
|
||||||
|
|
||||||
queueProcessor := sync.NewSyncQueueProcessor(queries)
|
queueProcessor := sync.NewSyncQueueProcessor(queries)
|
||||||
queueProcessor.SetProgressService(progressService)
|
queueProcessor.SetProgressService(progressService)
|
||||||
|
queueProcessor.SetAnnotationService(annotationService)
|
||||||
|
|
||||||
// Create library service
|
// Create library service
|
||||||
libraryService := services.NewLibraryService(queries)
|
libraryService := services.NewLibraryService(queries)
|
||||||
@@ -79,6 +83,7 @@ func main() {
|
|||||||
|
|
||||||
koreaderHandler := handlers.NewKOReaderHandler(queries, connManager, queueProcessor)
|
koreaderHandler := handlers.NewKOReaderHandler(queries, connManager, queueProcessor)
|
||||||
koreaderHandler.SetProgressService(progressService)
|
koreaderHandler.SetProgressService(progressService)
|
||||||
|
koreaderHandler.SetAnnotationService(annotationService)
|
||||||
koreaderHandler.SetLibraryService(libraryService)
|
koreaderHandler.SetLibraryService(libraryService)
|
||||||
wsHandler := handlers.NewWSHandler(queries, connManager, cfg.JWTSecret, deviceAuthMiddleware)
|
wsHandler := handlers.NewWSHandler(queries, connManager, cfg.JWTSecret, deviceAuthMiddleware)
|
||||||
conflictHandler := handlers.NewConflictHandler(queries, connManager)
|
conflictHandler := handlers.NewConflictHandler(queries, connManager)
|
||||||
@@ -95,6 +100,7 @@ func main() {
|
|||||||
filtersHandler := handlers.NewFiltersHandler(queries)
|
filtersHandler := handlers.NewFiltersHandler(queries)
|
||||||
mediaHandler := handlers.NewMediaHandler(queries, libraryService, worker)
|
mediaHandler := handlers.NewMediaHandler(queries, libraryService, worker)
|
||||||
mediaHandler.SetProgressService(progressService)
|
mediaHandler.SetProgressService(progressService)
|
||||||
|
mediaHandler.SetAnnotationService(annotationService)
|
||||||
matchingHandler := handlers.NewMatchingHandler(queries, connManager)
|
matchingHandler := handlers.NewMatchingHandler(queries, connManager)
|
||||||
jobsHandler := handlers.NewJobsHandler(queries, worker)
|
jobsHandler := handlers.NewJobsHandler(queries, worker)
|
||||||
|
|
||||||
@@ -162,6 +168,7 @@ func main() {
|
|||||||
ConnManager: connManager,
|
ConnManager: connManager,
|
||||||
QueueProcessor: queueProcessor,
|
QueueProcessor: queueProcessor,
|
||||||
ProgressService: progressService,
|
ProgressService: progressService,
|
||||||
|
AnnotationService: annotationService,
|
||||||
DeviceAuthMiddleware: deviceAuthMiddleware,
|
DeviceAuthMiddleware: deviceAuthMiddleware,
|
||||||
JobsHandler: jobsHandler,
|
JobsHandler: jobsHandler,
|
||||||
LoginTracker: loginAttemptTracker,
|
LoginTracker: loginAttemptTracker,
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ type TestServerSetup struct {
|
|||||||
ConnManager *wsync.ConnectionManager
|
ConnManager *wsync.ConnectionManager
|
||||||
QueueProcessor *wsync.SyncQueueProcessor
|
QueueProcessor *wsync.SyncQueueProcessor
|
||||||
ProgressService *wsync.ProgressService
|
ProgressService *wsync.ProgressService
|
||||||
|
AnnotationService *wsync.AnnotationService
|
||||||
CleanupCancel context.CancelFunc
|
CleanupCancel context.CancelFunc
|
||||||
QueueCtx context.Context
|
QueueCtx context.Context
|
||||||
QueueCancel context.CancelFunc
|
QueueCancel context.CancelFunc
|
||||||
@@ -455,6 +456,7 @@ func setupTestServer(t *testing.T) *TestServerSetup {
|
|||||||
cleanupCancel := connManager.StartCleanupTask()
|
cleanupCancel := connManager.StartCleanupTask()
|
||||||
|
|
||||||
progressService := wsync.NewProgressService(queries, connManager)
|
progressService := wsync.NewProgressService(queries, connManager)
|
||||||
|
annotationService := wsync.NewAnnotationService(queries, connManager)
|
||||||
|
|
||||||
queueProcessor := wsync.NewSyncQueueProcessor(queries)
|
queueProcessor := wsync.NewSyncQueueProcessor(queries)
|
||||||
queueProcessor.SetProgressService(progressService)
|
queueProcessor.SetProgressService(progressService)
|
||||||
@@ -463,6 +465,7 @@ func setupTestServer(t *testing.T) *TestServerSetup {
|
|||||||
|
|
||||||
koreaderHandler := handlers.NewKOReaderHandler(queries, connManager, queueProcessor)
|
koreaderHandler := handlers.NewKOReaderHandler(queries, connManager, queueProcessor)
|
||||||
koreaderHandler.SetProgressService(progressService)
|
koreaderHandler.SetProgressService(progressService)
|
||||||
|
koreaderHandler.SetAnnotationService(annotationService)
|
||||||
wsHandler := handlers.NewWSHandler(queries, connManager, cfg.JWTSecret, deviceAuthMiddleware)
|
wsHandler := handlers.NewWSHandler(queries, connManager, cfg.JWTSecret, deviceAuthMiddleware)
|
||||||
conflictHandler := handlers.NewConflictHandler(queries, connManager)
|
conflictHandler := handlers.NewConflictHandler(queries, connManager)
|
||||||
analyticsHandler := handlers.NewAnalyticsHandler(queries)
|
analyticsHandler := handlers.NewAnalyticsHandler(queries)
|
||||||
@@ -482,6 +485,7 @@ func setupTestServer(t *testing.T) *TestServerSetup {
|
|||||||
seriesHandler := handlers.NewSeriesHandler(queries)
|
seriesHandler := handlers.NewSeriesHandler(queries)
|
||||||
mediaHandler := handlers.NewMediaHandler(queries, libraryService, worker)
|
mediaHandler := handlers.NewMediaHandler(queries, libraryService, worker)
|
||||||
mediaHandler.SetProgressService(progressService)
|
mediaHandler.SetProgressService(progressService)
|
||||||
|
mediaHandler.SetAnnotationService(annotationService)
|
||||||
matchingHandler := handlers.NewMatchingHandler(queries, connManager)
|
matchingHandler := handlers.NewMatchingHandler(queries, connManager)
|
||||||
|
|
||||||
// Create conversion service for OPDS
|
// Create conversion service for OPDS
|
||||||
@@ -537,6 +541,7 @@ func setupTestServer(t *testing.T) *TestServerSetup {
|
|||||||
ConnManager: connManager,
|
ConnManager: connManager,
|
||||||
QueueProcessor: queueProcessor,
|
QueueProcessor: queueProcessor,
|
||||||
ProgressService: progressService,
|
ProgressService: progressService,
|
||||||
|
AnnotationService: annotationService,
|
||||||
DeviceAuthMiddleware: deviceAuthMiddleware,
|
DeviceAuthMiddleware: deviceAuthMiddleware,
|
||||||
LoginTracker: loginAttemptTracker,
|
LoginTracker: loginAttemptTracker,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ func (h *ConflictHandler) ResolveConflict(c *echo.Context) error {
|
|||||||
return echo.NewHTTPError(http.StatusForbidden, "access denied")
|
return echo.NewHTTPError(http.StatusForbidden, "access denied")
|
||||||
}
|
}
|
||||||
|
|
||||||
if conflict.ResolutionStatus.String != "unresolved" {
|
if conflict.ResolutionStatus.String == "user_resolved" {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "conflict already resolved")
|
return echo.NewHTTPError(http.StatusBadRequest, "conflict already resolved")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,6 +258,12 @@ func (h *ConflictHandler) ResolveConflict(c *echo.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if conflict.ConflictType == "annotation_highlight" || conflict.ConflictType == "annotation_bookmark" || conflict.ConflictType == "annotation_note" {
|
||||||
|
if err := h.applyAnnotationResolution(conflict.MediaItemID, conflict.UserID, winnerData, conflict.ConflictType); err == nil {
|
||||||
|
appliedTo["annotations"] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resolutionData := map[string]interface{}{
|
resolutionData := map[string]interface{}{
|
||||||
"winner": req.Winner,
|
"winner": req.Winner,
|
||||||
"applied_to": appliedTo,
|
"applied_to": appliedTo,
|
||||||
@@ -356,6 +362,143 @@ func (h *ConflictHandler) applyProgressResolution(mediaItemID pgtype.UUID, userI
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *ConflictHandler) applyAnnotationResolution(mediaItemID pgtype.UUID, userID pgtype.UUID, winnerData map[string]interface{}, conflictType string) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
dedupKey, _ := winnerData["dedup_key"].(string)
|
||||||
|
if dedupKey == "" {
|
||||||
|
return errors.New("missing dedup_key in winner data")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch conflictType {
|
||||||
|
case "annotation_highlight":
|
||||||
|
return h.applyHighlightResolution(ctx, mediaItemID, userID, dedupKey, winnerData)
|
||||||
|
case "annotation_bookmark":
|
||||||
|
return h.applyBookmarkResolution(ctx, mediaItemID, userID, dedupKey, winnerData)
|
||||||
|
case "annotation_note":
|
||||||
|
return h.applyNoteResolution(ctx, mediaItemID, userID, dedupKey, winnerData)
|
||||||
|
default:
|
||||||
|
return errors.New("unknown annotation conflict type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ConflictHandler) applyHighlightResolution(ctx context.Context, mediaItemID pgtype.UUID, userID pgtype.UUID, dedupKey string, data map[string]interface{}) error {
|
||||||
|
existing, err := h.db.GetMediaHighlightByDedupKey(ctx, database.GetMediaHighlightByDedupKeyParams{
|
||||||
|
MediaItemID: mediaItemID,
|
||||||
|
DedupKey: pgtype.Text{String: dedupKey, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
params := database.UpdateMediaHighlightForSyncParams{
|
||||||
|
ID: existing.ID,
|
||||||
|
SelectionText: existing.SelectionText,
|
||||||
|
StartPosition: existing.StartPosition,
|
||||||
|
EndPosition: existing.EndPosition,
|
||||||
|
Color: existing.Color,
|
||||||
|
NoteText: existing.NoteText,
|
||||||
|
PercentageStart: existing.PercentageStart,
|
||||||
|
PercentageEnd: existing.PercentageEnd,
|
||||||
|
EpubcfiStart: existing.EpubcfiStart,
|
||||||
|
EpubcfiEnd: existing.EpubcfiEnd,
|
||||||
|
ChapterReference: existing.ChapterReference,
|
||||||
|
LastModifiedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true},
|
||||||
|
LastModifiedSource: pgtype.Text{String: "conflict_resolution", Valid: true},
|
||||||
|
DeviceSyncData: existing.DeviceSyncData,
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := data["selection_text"].(string); ok {
|
||||||
|
params.SelectionText = v
|
||||||
|
}
|
||||||
|
if v, ok := data["color"].(string); ok {
|
||||||
|
params.Color = pgtype.Text{String: v, Valid: true}
|
||||||
|
}
|
||||||
|
if v, ok := data["note_text"].(string); ok {
|
||||||
|
params.NoteText = pgtype.Text{String: v, Valid: true}
|
||||||
|
}
|
||||||
|
if v, ok := data["start_position"].(string); ok {
|
||||||
|
params.StartPosition = pgtype.Text{String: v, Valid: true}
|
||||||
|
}
|
||||||
|
if v, ok := data["end_position"].(string); ok {
|
||||||
|
params.EndPosition = pgtype.Text{String: v, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = h.db.UpdateMediaHighlightForSync(ctx, params)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ConflictHandler) applyBookmarkResolution(ctx context.Context, mediaItemID pgtype.UUID, userID pgtype.UUID, dedupKey string, data map[string]interface{}) error {
|
||||||
|
existing, err := h.db.GetMediaBookmarkByDedupKey(ctx, database.GetMediaBookmarkByDedupKeyParams{
|
||||||
|
MediaItemID: mediaItemID,
|
||||||
|
DedupKey: pgtype.Text{String: dedupKey, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
params := database.UpdateMediaBookmarkForSyncParams{
|
||||||
|
ID: existing.ID,
|
||||||
|
PageNumber: existing.PageNumber,
|
||||||
|
ChapterNumber: existing.ChapterNumber,
|
||||||
|
CfiPosition: existing.CfiPosition,
|
||||||
|
Title: existing.Title,
|
||||||
|
Position: existing.Position,
|
||||||
|
Notes: existing.Notes,
|
||||||
|
PercentageLocation: existing.PercentageLocation,
|
||||||
|
EpubcfiLocation: existing.EpubcfiLocation,
|
||||||
|
ChapterReference: existing.ChapterReference,
|
||||||
|
LastModifiedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true},
|
||||||
|
LastModifiedSource: pgtype.Text{String: "conflict_resolution", Valid: true},
|
||||||
|
DeviceSyncData: existing.DeviceSyncData,
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := data["title"].(string); ok {
|
||||||
|
params.Title = v
|
||||||
|
}
|
||||||
|
if v, ok := data["notes"].(string); ok {
|
||||||
|
params.Notes = pgtype.Text{String: v, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = h.db.UpdateMediaBookmarkForSync(ctx, params)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ConflictHandler) applyNoteResolution(ctx context.Context, mediaItemID pgtype.UUID, userID pgtype.UUID, dedupKey string, data map[string]interface{}) error {
|
||||||
|
existing, err := h.db.GetMediaNoteByDedupKey(ctx, database.GetMediaNoteByDedupKeyParams{
|
||||||
|
MediaItemID: mediaItemID,
|
||||||
|
DedupKey: pgtype.Text{String: dedupKey, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
params := database.UpdateMediaNoteForSyncParams{
|
||||||
|
ID: existing.ID,
|
||||||
|
Content: existing.Content,
|
||||||
|
Position: existing.Position,
|
||||||
|
PercentageLocation: existing.PercentageLocation,
|
||||||
|
CharacterStart: existing.CharacterStart,
|
||||||
|
CharacterEnd: existing.CharacterEnd,
|
||||||
|
EpubcfiLocation: existing.EpubcfiLocation,
|
||||||
|
ChapterReference: existing.ChapterReference,
|
||||||
|
ParagraphReference: existing.ParagraphReference,
|
||||||
|
LastModifiedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true},
|
||||||
|
LastModifiedSource: pgtype.Text{String: "conflict_resolution", Valid: true},
|
||||||
|
DeviceSyncData: existing.DeviceSyncData,
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := data["content"].(string); ok {
|
||||||
|
params.Content = v
|
||||||
|
}
|
||||||
|
if v, ok := data["position"].(string); ok {
|
||||||
|
params.Position = pgtype.Text{String: v, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = h.db.UpdateMediaNoteForSync(ctx, params)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (h *ConflictHandler) notifyDevicesOfResolution(mediaItemID pgtype.UUID, data map[string]interface{}) []string {
|
func (h *ConflictHandler) notifyDevicesOfResolution(mediaItemID pgtype.UUID, data map[string]interface{}) []string {
|
||||||
devices, err := h.db.ListDevicesByType(context.Background(), "koreader")
|
devices, err := h.db.ListDevicesByType(context.Background(), "koreader")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+170
-1
@@ -3,6 +3,7 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"bookhoard/internal/database"
|
"bookhoard/internal/database"
|
||||||
wsync "bookhoard/internal/sync"
|
wsync "bookhoard/internal/sync"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -19,6 +20,7 @@ type KoboHandler struct {
|
|||||||
db *database.Queries
|
db *database.Queries
|
||||||
connManager *wsync.ConnectionManager
|
connManager *wsync.ConnectionManager
|
||||||
progressSvc *wsync.ProgressService
|
progressSvc *wsync.ProgressService
|
||||||
|
annotationSvc *wsync.AnnotationService
|
||||||
libraryService LibraryPathResolver
|
libraryService LibraryPathResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +32,10 @@ func (h *KoboHandler) SetProgressService(svc *wsync.ProgressService) {
|
|||||||
h.progressSvc = svc
|
h.progressSvc = svc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *KoboHandler) SetAnnotationService(svc *wsync.AnnotationService) {
|
||||||
|
h.annotationSvc = svc
|
||||||
|
}
|
||||||
|
|
||||||
func (h *KoboHandler) SetLibraryService(svc LibraryPathResolver) {
|
func (h *KoboHandler) SetLibraryService(svc LibraryPathResolver) {
|
||||||
h.libraryService = svc
|
h.libraryService = svc
|
||||||
}
|
}
|
||||||
@@ -248,6 +254,13 @@ type KoboSyncStatus struct {
|
|||||||
Status string `json:"Status"`
|
Status string `json:"Status"`
|
||||||
MarkupsSynced int `json:"MarkupsSynced"`
|
MarkupsSynced int `json:"MarkupsSynced"`
|
||||||
BookmarksSynced int `json:"BookmarksSynced"`
|
BookmarksSynced int `json:"BookmarksSynced"`
|
||||||
|
DeletedAnnotations []KoboDeletedAnnotation `json:"DeletedAnnotations,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type KoboDeletedAnnotation struct {
|
||||||
|
ContentId string `json:"ContentId"`
|
||||||
|
BookmarkId string `json:"BookmarkId"`
|
||||||
|
Type string `json:"Type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type KoboServerSyncData struct {
|
type KoboServerSyncData struct {
|
||||||
@@ -311,7 +324,7 @@ func (h *KoboHandler) Initialization(c *echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bookmarkCount := 0
|
bookmarkCount := 0
|
||||||
annotations, _ := h.db.GetAnnotationsForBook(c.Request().Context(), database.GetAnnotationsForBookParams{
|
annotations, _ := h.db.GetActiveAnnotationsForBook(c.Request().Context(), database.GetActiveAnnotationsForBookParams{
|
||||||
MediaItemID: pgtype.UUID{Bytes: item.ID.Bytes, Valid: true},
|
MediaItemID: pgtype.UUID{Bytes: item.ID.Bytes, Valid: true},
|
||||||
UserID: pgUserID,
|
UserID: pgUserID,
|
||||||
})
|
})
|
||||||
@@ -403,6 +416,7 @@ func (h *KoboHandler) Markup(c *echo.Context) error {
|
|||||||
markupsSynced := 0
|
markupsSynced := 0
|
||||||
bookmarksSynced := 0
|
bookmarksSynced := 0
|
||||||
unlinkedBooks := 0
|
unlinkedBooks := 0
|
||||||
|
processedBooks := make(map[pgtype.UUID]string)
|
||||||
|
|
||||||
for _, readingSync := range req.ReadingSync {
|
for _, readingSync := range req.ReadingSync {
|
||||||
bookhoardUUID, err, _ := h.mapContentIdToBookhoardUUID(c, readingSync.ContentId, deviceUUID)
|
bookhoardUUID, err, _ := h.mapContentIdToBookhoardUUID(c, readingSync.ContentId, deviceUUID)
|
||||||
@@ -412,6 +426,7 @@ func (h *KoboHandler) Markup(c *echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pgMediaUUID := pgtype.UUID{Bytes: bookhoardUUID, Valid: true}
|
pgMediaUUID := pgtype.UUID{Bytes: bookhoardUUID, Valid: true}
|
||||||
|
processedBooks[pgMediaUUID] = readingSync.ContentId
|
||||||
percentage := readingSync.PercentRead / 100.0
|
percentage := readingSync.PercentRead / 100.0
|
||||||
|
|
||||||
// Kobo only sends a percentage. For fixed-layout & comic formats the page
|
// Kobo only sends a percentage. For fixed-layout & comic formats the page
|
||||||
@@ -465,10 +480,32 @@ func (h *KoboHandler) Markup(c *echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pgMediaUUID := pgtype.UUID{Bytes: bookhoardUUID, Valid: true}
|
pgMediaUUID := pgtype.UUID{Bytes: bookhoardUUID, Valid: true}
|
||||||
|
processedBooks[pgMediaUUID] = bookmarkSync.ContentId
|
||||||
|
|
||||||
switch bookmarkSync.BookmarkType {
|
switch bookmarkSync.BookmarkType {
|
||||||
case "annotation":
|
case "annotation":
|
||||||
if bookmarkSync.BookmarkText != "" {
|
if bookmarkSync.BookmarkText != "" {
|
||||||
|
if h.annotationSvc != nil {
|
||||||
|
deviceData, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"bookmark_id": bookmarkSync.BookmarkId,
|
||||||
|
"date_created": bookmarkSync.DateCreated,
|
||||||
|
})
|
||||||
|
|
||||||
|
result, err := h.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
|
||||||
|
MediaItemID: pgMediaUUID,
|
||||||
|
UserID: pgUserID,
|
||||||
|
SelectionText: bookmarkSync.BookmarkText,
|
||||||
|
StartPosition: bookmarkSync.BookmarkId,
|
||||||
|
EndPosition: bookmarkSync.BookmarkId,
|
||||||
|
Color: "#ffff00",
|
||||||
|
NoteText: bookmarkSync.BookmarkTitle,
|
||||||
|
Source: "kobo",
|
||||||
|
DeviceSyncData: deviceData,
|
||||||
|
})
|
||||||
|
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
||||||
|
bookmarksSynced++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
h.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
|
h.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
|
||||||
MediaItemID: pgMediaUUID,
|
MediaItemID: pgMediaUUID,
|
||||||
UserID: pgUserID,
|
UserID: pgUserID,
|
||||||
@@ -479,8 +516,28 @@ func (h *KoboHandler) Markup(c *echo.Context) error {
|
|||||||
})
|
})
|
||||||
bookmarksSynced++
|
bookmarksSynced++
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case "bookmark":
|
case "bookmark":
|
||||||
if bookmarkSync.BookmarkText != "" {
|
if bookmarkSync.BookmarkText != "" {
|
||||||
|
if h.annotationSvc != nil {
|
||||||
|
deviceData, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"bookmark_id": bookmarkSync.BookmarkId,
|
||||||
|
"date_created": bookmarkSync.DateCreated,
|
||||||
|
})
|
||||||
|
|
||||||
|
result, err := h.annotationSvc.SaveBookmark(c.Request().Context(), wsync.SaveBookmarkRequest{
|
||||||
|
MediaItemID: pgMediaUUID,
|
||||||
|
UserID: pgUserID,
|
||||||
|
Title: bookmarkSync.BookmarkText,
|
||||||
|
Position: bookmarkSync.BookmarkId,
|
||||||
|
ChapterNumber: int32(bookmarkSync.Chapter),
|
||||||
|
Source: "kobo",
|
||||||
|
DeviceSyncData: deviceData,
|
||||||
|
})
|
||||||
|
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
||||||
|
bookmarksSynced++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
h.db.CreateMediaNote(c.Request().Context(), database.CreateMediaNoteParams{
|
h.db.CreateMediaNote(c.Request().Context(), database.CreateMediaNoteParams{
|
||||||
MediaItemID: pgMediaUUID,
|
MediaItemID: pgMediaUUID,
|
||||||
UserID: pgUserID,
|
UserID: pgUserID,
|
||||||
@@ -489,6 +546,7 @@ func (h *KoboHandler) Markup(c *echo.Context) error {
|
|||||||
})
|
})
|
||||||
bookmarksSynced++
|
bookmarksSynced++
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case "last-read-place":
|
case "last-read-place":
|
||||||
if bookmarkSync.BookmarkId != "" {
|
if bookmarkSync.BookmarkId != "" {
|
||||||
var epubcfi string
|
var epubcfi string
|
||||||
@@ -564,6 +622,32 @@ func (h *KoboHandler) Markup(c *echo.Context) error {
|
|||||||
BookmarksSynced: bookmarksSynced,
|
BookmarksSynced: bookmarksSynced,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if h.annotationSvc != nil && len(processedBooks) > 0 {
|
||||||
|
cutoff := pgtype.Timestamptz{Time: time.Now().Add(-wsync.TombstoneTTL), Valid: true}
|
||||||
|
for mediaItemID, contentId := range processedBooks {
|
||||||
|
tombstones, _ := h.db.GetTombstonedAnnotationsForBook(c.Request().Context(), database.GetTombstonedAnnotationsForBookParams{
|
||||||
|
MediaItemID: mediaItemID,
|
||||||
|
UserID: pgUserID,
|
||||||
|
DeletedAt: cutoff,
|
||||||
|
})
|
||||||
|
for _, ts := range tombstones {
|
||||||
|
var dd map[string]interface{}
|
||||||
|
if len(ts.DeviceSyncData) > 0 {
|
||||||
|
json.Unmarshal(ts.DeviceSyncData, &dd)
|
||||||
|
}
|
||||||
|
bookmarkID, _ := dd["bookmark_id"].(string)
|
||||||
|
if bookmarkID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
response.DeletedAnnotations = append(response.DeletedAnnotations, KoboDeletedAnnotation{
|
||||||
|
ContentId: contentId,
|
||||||
|
BookmarkId: bookmarkID,
|
||||||
|
Type: ts.AnnotationType,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Include unlinked books count if any
|
// Include unlinked books count if any
|
||||||
if unlinkedBooks > 0 {
|
if unlinkedBooks > 0 {
|
||||||
// For now, just log it. In production, this should trigger an alert
|
// For now, just log it. In production, this should trigger an alert
|
||||||
@@ -606,6 +690,27 @@ func (h *KoboHandler) Bookmark(c *echo.Context) error {
|
|||||||
switch bookmarkSync.BookmarkType {
|
switch bookmarkSync.BookmarkType {
|
||||||
case "annotation":
|
case "annotation":
|
||||||
if bookmarkSync.BookmarkText != "" {
|
if bookmarkSync.BookmarkText != "" {
|
||||||
|
if h.annotationSvc != nil {
|
||||||
|
deviceData, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"bookmark_id": bookmarkSync.BookmarkId,
|
||||||
|
"date_created": bookmarkSync.DateCreated,
|
||||||
|
})
|
||||||
|
|
||||||
|
result, err := h.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
|
||||||
|
MediaItemID: pgMediaUUID,
|
||||||
|
UserID: pgUserID,
|
||||||
|
SelectionText: bookmarkSync.BookmarkText,
|
||||||
|
StartPosition: bookmarkSync.BookmarkId,
|
||||||
|
EndPosition: bookmarkSync.BookmarkId,
|
||||||
|
Color: "#ffff00",
|
||||||
|
NoteText: bookmarkSync.BookmarkTitle,
|
||||||
|
Source: "kobo",
|
||||||
|
DeviceSyncData: deviceData,
|
||||||
|
})
|
||||||
|
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
||||||
|
bookmarksSynced++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
h.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
|
h.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
|
||||||
MediaItemID: pgMediaUUID,
|
MediaItemID: pgMediaUUID,
|
||||||
UserID: pgUserID,
|
UserID: pgUserID,
|
||||||
@@ -616,8 +721,28 @@ func (h *KoboHandler) Bookmark(c *echo.Context) error {
|
|||||||
})
|
})
|
||||||
bookmarksSynced++
|
bookmarksSynced++
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case "bookmark":
|
case "bookmark":
|
||||||
if bookmarkSync.BookmarkText != "" {
|
if bookmarkSync.BookmarkText != "" {
|
||||||
|
if h.annotationSvc != nil {
|
||||||
|
deviceData, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"bookmark_id": bookmarkSync.BookmarkId,
|
||||||
|
"date_created": bookmarkSync.DateCreated,
|
||||||
|
})
|
||||||
|
|
||||||
|
result, err := h.annotationSvc.SaveBookmark(c.Request().Context(), wsync.SaveBookmarkRequest{
|
||||||
|
MediaItemID: pgMediaUUID,
|
||||||
|
UserID: pgUserID,
|
||||||
|
Title: bookmarkSync.BookmarkText,
|
||||||
|
Position: bookmarkSync.BookmarkId,
|
||||||
|
ChapterNumber: int32(bookmarkSync.Chapter),
|
||||||
|
Source: "kobo",
|
||||||
|
DeviceSyncData: deviceData,
|
||||||
|
})
|
||||||
|
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
||||||
|
bookmarksSynced++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
h.db.CreateMediaNote(c.Request().Context(), database.CreateMediaNoteParams{
|
h.db.CreateMediaNote(c.Request().Context(), database.CreateMediaNoteParams{
|
||||||
MediaItemID: pgMediaUUID,
|
MediaItemID: pgMediaUUID,
|
||||||
UserID: pgUserID,
|
UserID: pgUserID,
|
||||||
@@ -628,6 +753,7 @@ func (h *KoboHandler) Bookmark(c *echo.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_, err := h.db.UpdateDeviceLastSync(c.Request().Context(), device.ID)
|
_, err := h.db.UpdateDeviceLastSync(c.Request().Context(), device.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -756,6 +882,18 @@ func (h *KoboHandler) SyncFromServer(c *echo.Context) error {
|
|||||||
|
|
||||||
for _, bookmark := range syncData.Bookmarks {
|
for _, bookmark := range syncData.Bookmarks {
|
||||||
if bookmark.BookmarkType == "bookmark" {
|
if bookmark.BookmarkType == "bookmark" {
|
||||||
|
if h.annotationSvc != nil {
|
||||||
|
result, err := h.annotationSvc.SaveBookmark(c.Request().Context(), wsync.SaveBookmarkRequest{
|
||||||
|
MediaItemID: pgMediaUUID,
|
||||||
|
UserID: pgUserID,
|
||||||
|
Title: bookmark.BookmarkText,
|
||||||
|
Position: bookmark.BookmarkId,
|
||||||
|
Source: "kobo",
|
||||||
|
})
|
||||||
|
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
||||||
|
bookmarksSent++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
h.db.CreateMediaNote(c.Request().Context(), database.CreateMediaNoteParams{
|
h.db.CreateMediaNote(c.Request().Context(), database.CreateMediaNoteParams{
|
||||||
MediaItemID: pgMediaUUID,
|
MediaItemID: pgMediaUUID,
|
||||||
UserID: pgUserID,
|
UserID: pgUserID,
|
||||||
@@ -763,7 +901,22 @@ func (h *KoboHandler) SyncFromServer(c *echo.Context) error {
|
|||||||
Position: pgtype.Text{String: bookmark.BookmarkId, Valid: true},
|
Position: pgtype.Text{String: bookmark.BookmarkId, Valid: true},
|
||||||
})
|
})
|
||||||
bookmarksSent++
|
bookmarksSent++
|
||||||
|
}
|
||||||
} else if bookmark.BookmarkType == "annotation" {
|
} else if bookmark.BookmarkType == "annotation" {
|
||||||
|
if h.annotationSvc != nil {
|
||||||
|
result, err := h.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
|
||||||
|
MediaItemID: pgMediaUUID,
|
||||||
|
UserID: pgUserID,
|
||||||
|
SelectionText: bookmark.BookmarkText,
|
||||||
|
StartPosition: bookmark.BookmarkId,
|
||||||
|
EndPosition: bookmark.BookmarkId,
|
||||||
|
Color: "#ffff00",
|
||||||
|
Source: "kobo",
|
||||||
|
})
|
||||||
|
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
||||||
|
highlightsSent++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
h.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
|
h.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
|
||||||
MediaItemID: pgMediaUUID,
|
MediaItemID: pgMediaUUID,
|
||||||
UserID: pgUserID,
|
UserID: pgUserID,
|
||||||
@@ -775,8 +928,23 @@ func (h *KoboHandler) SyncFromServer(c *echo.Context) error {
|
|||||||
highlightsSent++
|
highlightsSent++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, highlight := range syncData.Highlights {
|
for _, highlight := range syncData.Highlights {
|
||||||
|
if h.annotationSvc != nil {
|
||||||
|
result, err := h.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
|
||||||
|
MediaItemID: pgMediaUUID,
|
||||||
|
UserID: pgUserID,
|
||||||
|
SelectionText: highlight.BookmarkText,
|
||||||
|
StartPosition: highlight.BookmarkId,
|
||||||
|
EndPosition: highlight.BookmarkId,
|
||||||
|
Color: "#ffff00",
|
||||||
|
Source: "kobo",
|
||||||
|
})
|
||||||
|
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
||||||
|
highlightsSent++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
h.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
|
h.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
|
||||||
MediaItemID: pgMediaUUID,
|
MediaItemID: pgMediaUUID,
|
||||||
UserID: pgUserID,
|
UserID: pgUserID,
|
||||||
@@ -788,6 +956,7 @@ func (h *KoboHandler) SyncFromServer(c *echo.Context) error {
|
|||||||
highlightsSent++
|
highlightsSent++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_, err := h.db.UpdateDeviceLastSync(c.Request().Context(), device.ID)
|
_, err := h.db.UpdateDeviceLastSync(c.Request().Context(), device.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bookhoard/internal/database"
|
"bookhoard/internal/database"
|
||||||
wsync "bookhoard/internal/sync"
|
wsync "bookhoard/internal/sync"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -19,6 +20,7 @@ type KOReaderHandler struct {
|
|||||||
connManager *wsync.ConnectionManager
|
connManager *wsync.ConnectionManager
|
||||||
queue *wsync.SyncQueueProcessor
|
queue *wsync.SyncQueueProcessor
|
||||||
progressSvc *wsync.ProgressService
|
progressSvc *wsync.ProgressService
|
||||||
|
annotationSvc *wsync.AnnotationService
|
||||||
libraryService LibraryPathResolver
|
libraryService LibraryPathResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +36,27 @@ func (h *KOReaderHandler) SetProgressService(svc *wsync.ProgressService) {
|
|||||||
h.progressSvc = svc
|
h.progressSvc = svc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *KOReaderHandler) SetAnnotationService(svc *wsync.AnnotationService) {
|
||||||
|
h.annotationSvc = svc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *KOReaderHandler) convertHighlightPositions(ctx context.Context, mediaItemID pgtype.UUID, pos0, pos1 string) (string, string) {
|
||||||
|
if pos0 == "" || h.libraryService == nil {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
mediaItem, err := h.db.GetMediaItem(ctx, mediaItemID)
|
||||||
|
if err != nil {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
epubPath, err := h.libraryService.ResolveMediaPath(ctx, mediaItem.LibraryID, mediaItem.FilePath)
|
||||||
|
if err != nil || epubPath == "" {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
startLoc := wsync.ConvertToCanonical(wsync.LocatorSourceKOReader, pos0, 0, "", mediaItem.FormatGroup, epubPath, "")
|
||||||
|
endLoc := wsync.ConvertToCanonical(wsync.LocatorSourceKOReader, pos1, 0, "", mediaItem.FormatGroup, epubPath, "")
|
||||||
|
return startLoc.CFI, endLoc.CFI
|
||||||
|
}
|
||||||
|
|
||||||
func (h *KOReaderHandler) SetLibraryService(svc LibraryPathResolver) {
|
func (h *KOReaderHandler) SetLibraryService(svc LibraryPathResolver) {
|
||||||
h.libraryService = svc
|
h.libraryService = svc
|
||||||
}
|
}
|
||||||
@@ -157,6 +180,8 @@ type KOReaderAnnotations struct {
|
|||||||
Highlights []KOReaderHighlight `json:"highlights,omitempty"`
|
Highlights []KOReaderHighlight `json:"highlights,omitempty"`
|
||||||
Notes []KOReaderNote `json:"notes,omitempty"`
|
Notes []KOReaderNote `json:"notes,omitempty"`
|
||||||
Bookmarks []KOReaderBookmark `json:"bookmarks,omitempty"`
|
Bookmarks []KOReaderBookmark `json:"bookmarks,omitempty"`
|
||||||
|
DeletedHighlights []map[string]interface{} `json:"deleted_highlights,omitempty"`
|
||||||
|
DeletedBookmarks []map[string]interface{} `json:"deleted_bookmarks,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type KOReaderLibraryResponse struct {
|
type KOReaderLibraryResponse struct {
|
||||||
@@ -396,6 +421,7 @@ func (h *KOReaderHandler) handleCheckpointSync(c *echo.Context, device database.
|
|||||||
if synced {
|
if synced {
|
||||||
booksEnqueued++
|
booksEnqueued++
|
||||||
}
|
}
|
||||||
|
h.processBookAnnotations(c.Request().Context(), device.ID, userID, mediaItemID, book)
|
||||||
bookResults = append(bookResults, KOReaderBookSyncResult{
|
bookResults = append(bookResults, KOReaderBookSyncResult{
|
||||||
SHA256: book.SHA256,
|
SHA256: book.SHA256,
|
||||||
BookUUID: uuid.UUID(mediaItemID.Bytes).String(),
|
BookUUID: uuid.UUID(mediaItemID.Bytes).String(),
|
||||||
@@ -441,6 +467,102 @@ func (h *KOReaderHandler) enqueueProgressForBook(c *echo.Context, deviceID pgtyp
|
|||||||
return h.queue.EnqueueProgress(update)
|
return h.queue.EnqueueProgress(update)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *KOReaderHandler) processBookAnnotations(ctx context.Context, deviceID, userID, mediaItemID pgtype.UUID, book KOReaderBookProgress) {
|
||||||
|
if h.annotationSvc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, hl := range book.Highlights {
|
||||||
|
startPos := hl.Pos0
|
||||||
|
endPos := hl.Pos1
|
||||||
|
epubcfiStart, epubcfiEnd := h.convertHighlightPositions(ctx, mediaItemID, startPos, endPos)
|
||||||
|
|
||||||
|
pctStart := 0.0
|
||||||
|
if hl.Percentage != nil {
|
||||||
|
pctStart = *hl.Percentage
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceData, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"datetime": hl.Datetime,
|
||||||
|
"pos0": hl.Pos0,
|
||||||
|
"pos1": hl.Pos1,
|
||||||
|
"page": hl.Page,
|
||||||
|
})
|
||||||
|
|
||||||
|
h.annotationSvc.SaveHighlight(ctx, wsync.SaveHighlightRequest{
|
||||||
|
MediaItemID: mediaItemID,
|
||||||
|
UserID: userID,
|
||||||
|
SelectionText: hl.Text,
|
||||||
|
StartPosition: startPos,
|
||||||
|
EndPosition: endPos,
|
||||||
|
Color: hl.Color,
|
||||||
|
NoteText: hl.Notes,
|
||||||
|
PercentageStart: pctStart,
|
||||||
|
EpubcfiStart: epubcfiStart,
|
||||||
|
EpubcfiEnd: epubcfiEnd,
|
||||||
|
Source: "koreader",
|
||||||
|
DeviceSyncData: deviceData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, note := range book.Notes {
|
||||||
|
startPos := note.Pos0
|
||||||
|
endPos := note.Pos1
|
||||||
|
epubcfiStart, epubcfiEnd := h.convertHighlightPositions(ctx, mediaItemID, startPos, endPos)
|
||||||
|
|
||||||
|
pctStart := 0.0
|
||||||
|
if note.Percentage != nil {
|
||||||
|
pctStart = *note.Percentage
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceData, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"datetime": note.Datetime,
|
||||||
|
"pos0": note.Pos0,
|
||||||
|
"pos1": note.Pos1,
|
||||||
|
"page": note.Page,
|
||||||
|
})
|
||||||
|
|
||||||
|
h.annotationSvc.SaveHighlight(ctx, wsync.SaveHighlightRequest{
|
||||||
|
MediaItemID: mediaItemID,
|
||||||
|
UserID: userID,
|
||||||
|
SelectionText: note.Text,
|
||||||
|
StartPosition: startPos,
|
||||||
|
EndPosition: endPos,
|
||||||
|
NoteText: note.Notes,
|
||||||
|
PercentageStart: pctStart,
|
||||||
|
EpubcfiStart: epubcfiStart,
|
||||||
|
EpubcfiEnd: epubcfiEnd,
|
||||||
|
Source: "koreader",
|
||||||
|
DeviceSyncData: deviceData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, bookmark := range book.Bookmarks {
|
||||||
|
position := ""
|
||||||
|
if bookmark.Pos0 != "" {
|
||||||
|
position = bookmark.Pos0
|
||||||
|
} else if bookmark.Page > 0 {
|
||||||
|
position = fmt.Sprintf("page:%d", bookmark.Page)
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceData, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"datetime": bookmark.Datetime,
|
||||||
|
"pos0": bookmark.Pos0,
|
||||||
|
"page": bookmark.Page,
|
||||||
|
})
|
||||||
|
|
||||||
|
h.annotationSvc.SaveBookmark(ctx, wsync.SaveBookmarkRequest{
|
||||||
|
MediaItemID: mediaItemID,
|
||||||
|
UserID: userID,
|
||||||
|
Title: bookmark.Text,
|
||||||
|
Position: position,
|
||||||
|
ChapterNumber: int32(bookmark.Chapter),
|
||||||
|
Source: "koreader",
|
||||||
|
DeviceSyncData: deviceData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *KOReaderHandler) updateProgressForBook(c *echo.Context, deviceID pgtype.UUID, 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()
|
ctx := c.Request().Context()
|
||||||
|
|
||||||
@@ -522,8 +644,12 @@ func (h *KOReaderHandler) updateProgressForBook(c *echo.Context, deviceID pgtype
|
|||||||
}
|
}
|
||||||
|
|
||||||
_, err := h.progressSvc.SaveProgress(ctx, saveReq)
|
_, err := h.progressSvc.SaveProgress(ctx, saveReq)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
h.processBookAnnotations(ctx, deviceID, userID, mediaItemID, book)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
_, err := h.db.UpdateUniversalProgress(ctx, database.UpdateUniversalProgressParams{
|
_, err := h.db.UpdateUniversalProgress(ctx, database.UpdateUniversalProgressParams{
|
||||||
MediaItemID: mediaItemID,
|
MediaItemID: mediaItemID,
|
||||||
@@ -558,6 +684,8 @@ func (h *KOReaderHandler) updateProgressForBook(c *echo.Context, deviceID pgtype
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
h.processBookAnnotations(ctx, deviceID, userID, mediaItemID, book)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -657,7 +785,7 @@ func (h *KOReaderHandler) GetMetadata(c *echo.Context) error {
|
|||||||
progressData.TotalPages = &progress
|
progressData.TotalPages = &progress
|
||||||
}
|
}
|
||||||
|
|
||||||
annotations, err := h.db.GetAnnotationsForBook(c.Request().Context(), database.GetAnnotationsForBookParams{
|
annotations, err := h.db.GetActiveAnnotationsForBook(c.Request().Context(), database.GetActiveAnnotationsForBookParams{
|
||||||
MediaItemID: pgBookUUID,
|
MediaItemID: pgBookUUID,
|
||||||
UserID: pgUserID,
|
UserID: pgUserID,
|
||||||
})
|
})
|
||||||
@@ -670,13 +798,29 @@ func (h *KOReaderHandler) GetMetadata(c *echo.Context) error {
|
|||||||
|
|
||||||
for _, ann := range annotations {
|
for _, ann := range annotations {
|
||||||
if ann.AnnotationType == "highlight" {
|
if ann.AnnotationType == "highlight" {
|
||||||
annotationsResponse.Highlights = append(annotationsResponse.Highlights, KOReaderHighlight{
|
pos0 := ann.StartPosition.String
|
||||||
|
pos1 := ann.EndPosition.String
|
||||||
|
if ann.EpubcfiStart.Valid && ann.EpubcfiStart.String != "" {
|
||||||
|
if converted := h.reverseConvertCFI(c, mediaItem, ann.EpubcfiStart.String); converted != "" {
|
||||||
|
pos0 = converted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ann.EpubcfiEnd.Valid && ann.EpubcfiEnd.String != "" {
|
||||||
|
if converted := h.reverseConvertCFI(c, mediaItem, ann.EpubcfiEnd.String); converted != "" {
|
||||||
|
pos1 = converted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
highlight := KOReaderHighlight{
|
||||||
Text: ann.SelectionText,
|
Text: ann.SelectionText,
|
||||||
Pos0: ann.StartPosition.String,
|
Pos0: pos0,
|
||||||
Pos1: ann.EndPosition.String,
|
Pos1: pos1,
|
||||||
Color: ann.Color.String,
|
Color: ann.Color.String,
|
||||||
Datetime: ann.CreatedAt.Time.Format(time.RFC3339),
|
Datetime: ann.CreatedAt.Time.Format(time.RFC3339),
|
||||||
})
|
}
|
||||||
|
if ann.NoteText.Valid && ann.NoteText.String != "" {
|
||||||
|
highlight.Notes = ann.NoteText.String
|
||||||
|
}
|
||||||
|
annotationsResponse.Highlights = append(annotationsResponse.Highlights, highlight)
|
||||||
} else if ann.AnnotationType == "note" {
|
} else if ann.AnnotationType == "note" {
|
||||||
annotationsResponse.Notes = append(annotationsResponse.Notes, KOReaderNote{
|
annotationsResponse.Notes = append(annotationsResponse.Notes, KOReaderNote{
|
||||||
Text: ann.SelectionText,
|
Text: ann.SelectionText,
|
||||||
@@ -686,6 +830,52 @@ func (h *KOReaderHandler) GetMetadata(c *echo.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bookmarks, _ := h.db.GetMediaBookmarks(c.Request().Context(), database.GetMediaBookmarksParams{
|
||||||
|
MediaItemID: pgBookUUID,
|
||||||
|
UserID: pgUserID,
|
||||||
|
})
|
||||||
|
for _, bm := range bookmarks {
|
||||||
|
pos0 := bm.Position.String
|
||||||
|
if pos0 == "" && bm.CfiPosition.Valid {
|
||||||
|
pos0 = bm.CfiPosition.String
|
||||||
|
}
|
||||||
|
koreaderBookmark := KOReaderBookmark{
|
||||||
|
Text: bm.Title,
|
||||||
|
Pos0: pos0,
|
||||||
|
Pos1: pos0,
|
||||||
|
Datetime: bm.CreatedAt.Time.Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
if bm.Notes.Valid && bm.Notes.String != "" {
|
||||||
|
koreaderBookmark.Notes = bm.Notes.String
|
||||||
|
}
|
||||||
|
if bm.ChapterNumber.Valid {
|
||||||
|
koreaderBookmark.Chapter = int(bm.ChapterNumber.Int32)
|
||||||
|
}
|
||||||
|
annotationsResponse.Bookmarks = append(annotationsResponse.Bookmarks, koreaderBookmark)
|
||||||
|
}
|
||||||
|
|
||||||
|
cutoff := pgtype.Timestamptz{Time: time.Now().Add(-wsync.TombstoneTTL), Valid: true}
|
||||||
|
tombstones, _ := h.db.GetTombstonedAnnotationsForBook(c.Request().Context(), database.GetTombstonedAnnotationsForBookParams{
|
||||||
|
MediaItemID: pgBookUUID,
|
||||||
|
UserID: pgUserID,
|
||||||
|
DeletedAt: cutoff,
|
||||||
|
})
|
||||||
|
for _, ts := range tombstones {
|
||||||
|
var dd map[string]interface{}
|
||||||
|
if len(ts.DeviceSyncData) > 0 {
|
||||||
|
json.Unmarshal(ts.DeviceSyncData, &dd)
|
||||||
|
}
|
||||||
|
if dd == nil {
|
||||||
|
dd = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
dd["dedup_key"] = ts.DedupKey.String
|
||||||
|
if ts.AnnotationType == "highlight" {
|
||||||
|
annotationsResponse.DeletedHighlights = append(annotationsResponse.DeletedHighlights, dd)
|
||||||
|
} else if ts.AnnotationType == "bookmark" {
|
||||||
|
annotationsResponse.DeletedBookmarks = append(annotationsResponse.DeletedBookmarks, dd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
lastSync := "never"
|
lastSync := "never"
|
||||||
if progress.LastSyncTimestamp.Valid {
|
if progress.LastSyncTimestamp.Valid {
|
||||||
lastSync = progress.LastSyncTimestamp.Time.Format(time.RFC3339)
|
lastSync = progress.LastSyncTimestamp.Time.Format(time.RFC3339)
|
||||||
@@ -737,6 +927,21 @@ func (h *KOReaderHandler) convertCFIToXPointer(c *echo.Context, mediaItem databa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *KOReaderHandler) reverseConvertCFI(c *echo.Context, mediaItem database.MediaItems, epubcfi string) string {
|
||||||
|
if h.libraryService == nil || epubcfi == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
epubPath, err := h.libraryService.ResolveMediaPath(c.Request().Context(), mediaItem.LibraryID, mediaItem.FilePath)
|
||||||
|
if err != nil || epubPath == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
loc := wsync.ConvertFromCanonical(wsync.LocatorSourceKOReader, epubcfi, 0, "", mediaItem.FormatGroup, epubPath, "")
|
||||||
|
if loc.Position != "" && loc.Position != epubcfi {
|
||||||
|
return loc.Position
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func (h *KOReaderHandler) GetLibrary(c *echo.Context) error {
|
func (h *KOReaderHandler) GetLibrary(c *echo.Context) error {
|
||||||
device := c.Get("device").(database.Devices)
|
device := c.Get("device").(database.Devices)
|
||||||
userID := device.UserID.Bytes
|
userID := device.UserID.Bytes
|
||||||
@@ -776,7 +981,7 @@ func (h *KOReaderHandler) GetLibrary(c *echo.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
annotations, _ := h.db.GetAnnotationsForBook(c.Request().Context(), database.GetAnnotationsForBookParams{
|
annotations, _ := h.db.GetActiveAnnotationsForBook(c.Request().Context(), database.GetActiveAnnotationsForBookParams{
|
||||||
MediaItemID: pgItemUUID,
|
MediaItemID: pgItemUUID,
|
||||||
UserID: pgUserID,
|
UserID: pgUserID,
|
||||||
})
|
})
|
||||||
@@ -876,6 +1081,26 @@ func (h *KOReaderHandler) SyncBookmarks(c *echo.Context) error {
|
|||||||
position = fmt.Sprintf("page:%d", bookmark.Page)
|
position = fmt.Sprintf("page:%d", bookmark.Page)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if h.annotationSvc != nil {
|
||||||
|
deviceData, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"datetime": bookmark.Datetime,
|
||||||
|
"pos0": bookmark.Pos0,
|
||||||
|
"page": bookmark.Page,
|
||||||
|
})
|
||||||
|
|
||||||
|
result, err := h.annotationSvc.SaveBookmark(ctx, wsync.SaveBookmarkRequest{
|
||||||
|
MediaItemID: mediaItemID,
|
||||||
|
UserID: pgUserID,
|
||||||
|
Title: bookmark.Text,
|
||||||
|
Position: position,
|
||||||
|
ChapterNumber: int32(bookmark.Chapter),
|
||||||
|
Source: "koreader",
|
||||||
|
DeviceSyncData: deviceData,
|
||||||
|
})
|
||||||
|
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
||||||
|
bookmarksSynced++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
_, err := h.db.CreateMediaNote(ctx, database.CreateMediaNoteParams{
|
_, err := h.db.CreateMediaNote(ctx, database.CreateMediaNoteParams{
|
||||||
MediaItemID: mediaItemID,
|
MediaItemID: mediaItemID,
|
||||||
UserID: pgUserID,
|
UserID: pgUserID,
|
||||||
@@ -887,6 +1112,7 @@ func (h *KOReaderHandler) SyncBookmarks(c *echo.Context) error {
|
|||||||
bookmarksSynced++
|
bookmarksSynced++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, note := range req.Notes {
|
for _, note := range req.Notes {
|
||||||
mediaItemID := pgBookUUID
|
mediaItemID := pgBookUUID
|
||||||
@@ -906,6 +1132,25 @@ func (h *KOReaderHandler) SyncBookmarks(c *echo.Context) error {
|
|||||||
position = fmt.Sprintf("page:%d", note.Page)
|
position = fmt.Sprintf("page:%d", note.Page)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if h.annotationSvc != nil {
|
||||||
|
deviceData, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"datetime": note.Datetime,
|
||||||
|
"pos0": note.Pos0,
|
||||||
|
"page": note.Page,
|
||||||
|
})
|
||||||
|
|
||||||
|
result, err := h.annotationSvc.SaveNote(ctx, wsync.SaveNoteRequest{
|
||||||
|
MediaItemID: mediaItemID,
|
||||||
|
UserID: pgUserID,
|
||||||
|
Content: note.Notes,
|
||||||
|
Position: position,
|
||||||
|
Source: "koreader",
|
||||||
|
DeviceSyncData: deviceData,
|
||||||
|
})
|
||||||
|
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
||||||
|
notesSynced++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
_, err := h.db.CreateMediaNote(ctx, database.CreateMediaNoteParams{
|
_, err := h.db.CreateMediaNote(ctx, database.CreateMediaNoteParams{
|
||||||
MediaItemID: mediaItemID,
|
MediaItemID: mediaItemID,
|
||||||
UserID: pgUserID,
|
UserID: pgUserID,
|
||||||
@@ -917,6 +1162,7 @@ func (h *KOReaderHandler) SyncBookmarks(c *echo.Context) error {
|
|||||||
notesSynced++
|
notesSynced++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, highlight := range req.Highlights {
|
for _, highlight := range req.Highlights {
|
||||||
mediaItemID := pgBookUUID
|
mediaItemID := pgBookUUID
|
||||||
@@ -941,6 +1187,39 @@ func (h *KOReaderHandler) SyncBookmarks(c *echo.Context) error {
|
|||||||
color = highlight.Color
|
color = highlight.Color
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if h.annotationSvc != nil {
|
||||||
|
epubcfiStart, epubcfiEnd := h.convertHighlightPositions(ctx, mediaItemID, highlight.Pos0, highlight.Pos1)
|
||||||
|
|
||||||
|
pctStart := 0.0
|
||||||
|
if highlight.Percentage != nil {
|
||||||
|
pctStart = *highlight.Percentage
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceData, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"datetime": highlight.Datetime,
|
||||||
|
"pos0": highlight.Pos0,
|
||||||
|
"pos1": highlight.Pos1,
|
||||||
|
"page": highlight.Page,
|
||||||
|
})
|
||||||
|
|
||||||
|
result, err := h.annotationSvc.SaveHighlight(ctx, wsync.SaveHighlightRequest{
|
||||||
|
MediaItemID: mediaItemID,
|
||||||
|
UserID: pgUserID,
|
||||||
|
SelectionText: highlight.Text,
|
||||||
|
StartPosition: startPos,
|
||||||
|
EndPosition: endPos,
|
||||||
|
Color: color,
|
||||||
|
NoteText: highlight.Notes,
|
||||||
|
PercentageStart: pctStart,
|
||||||
|
EpubcfiStart: epubcfiStart,
|
||||||
|
EpubcfiEnd: epubcfiEnd,
|
||||||
|
Source: "koreader",
|
||||||
|
DeviceSyncData: deviceData,
|
||||||
|
})
|
||||||
|
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
||||||
|
highlightsSynced++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
_, err := h.db.CreateMediaHighlight(ctx, database.CreateMediaHighlightParams{
|
_, err := h.db.CreateMediaHighlight(ctx, database.CreateMediaHighlightParams{
|
||||||
MediaItemID: mediaItemID,
|
MediaItemID: mediaItemID,
|
||||||
UserID: pgUserID,
|
UserID: pgUserID,
|
||||||
@@ -954,6 +1233,7 @@ func (h *KOReaderHandler) SyncBookmarks(c *echo.Context) error {
|
|||||||
highlightsSynced++
|
highlightsSynced++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||||
"sync_status": "completed",
|
"sync_status": "completed",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
@@ -136,6 +137,7 @@ type MediaHandler struct {
|
|||||||
libraryService *services.LibraryService
|
libraryService *services.LibraryService
|
||||||
searchService *services.SearchService
|
searchService *services.SearchService
|
||||||
progressSvc *wsync.ProgressService
|
progressSvc *wsync.ProgressService
|
||||||
|
annotationSvc *wsync.AnnotationService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMediaHandler(db *database.Queries, libraryService *services.LibraryService, worker ...*services.Worker) *MediaHandler {
|
func NewMediaHandler(db *database.Queries, libraryService *services.LibraryService, worker ...*services.Worker) *MediaHandler {
|
||||||
@@ -154,6 +156,10 @@ func (mh *MediaHandler) SetProgressService(svc *wsync.ProgressService) {
|
|||||||
mh.progressSvc = svc
|
mh.progressSvc = svc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mh *MediaHandler) SetAnnotationService(svc *wsync.AnnotationService) {
|
||||||
|
mh.annotationSvc = svc
|
||||||
|
}
|
||||||
|
|
||||||
func (h *MediaHandler) DownloadBook(c *echo.Context) error {
|
func (h *MediaHandler) DownloadBook(c *echo.Context) error {
|
||||||
bookUUID, err := uuid.Parse(c.Param("uuid"))
|
bookUUID, err := uuid.Parse(c.Param("uuid"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1388,7 +1394,23 @@ func (mh *MediaHandler) CreateMediaNote(c *echo.Context) error {
|
|||||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||||
}
|
}
|
||||||
|
|
||||||
note, err := mh.db.CreateMediaNote(c.Request().Context(), database.CreateMediaNoteParams{
|
var note database.MediaNotes
|
||||||
|
if mh.annotationSvc != nil {
|
||||||
|
result, err := mh.annotationSvc.SaveNote(c.Request().Context(), wsync.SaveNoteRequest{
|
||||||
|
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
||||||
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
|
Content: req.Content,
|
||||||
|
Position: req.Position,
|
||||||
|
Source: "web",
|
||||||
|
ModifiedAt: time.Now(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
note = result.Note
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
note, err = mh.db.CreateMediaNote(c.Request().Context(), database.CreateMediaNoteParams{
|
||||||
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
||||||
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
Content: req.Content,
|
Content: req.Content,
|
||||||
@@ -1397,6 +1419,7 @@ func (mh *MediaHandler) CreateMediaNote(c *echo.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return c.JSON(http.StatusCreated, note)
|
return c.JSON(http.StatusCreated, note)
|
||||||
}
|
}
|
||||||
@@ -1456,7 +1479,11 @@ func (mh *MediaHandler) DeleteMediaNote(c *echo.Context) error {
|
|||||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if mh.annotationSvc != nil {
|
||||||
|
err = mh.annotationSvc.TombstoneNoteByID(c.Request().Context(), pgtype.UUID{Bytes: noteUUID, Valid: true})
|
||||||
|
} else {
|
||||||
err = mh.db.DeleteMediaNote(c.Request().Context(), pgtype.UUID{Bytes: noteUUID, Valid: true})
|
err = mh.db.DeleteMediaNote(c.Request().Context(), pgtype.UUID{Bytes: noteUUID, Valid: true})
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
}
|
}
|
||||||
@@ -1525,9 +1552,29 @@ func (mh *MediaHandler) CreateMediaHighlight(c *echo.Context) error {
|
|||||||
color = req.Color
|
color = req.Color
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pgMediaID := pgtype.UUID{Bytes: mediaUUID, Valid: true}
|
||||||
|
pgUserID := pgtype.UUID{Bytes: userUUID, Valid: true}
|
||||||
|
|
||||||
|
if mh.annotationSvc != nil {
|
||||||
|
result, err := mh.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
|
||||||
|
MediaItemID: pgMediaID,
|
||||||
|
UserID: pgUserID,
|
||||||
|
SelectionText: req.SelectionText,
|
||||||
|
StartPosition: req.StartPosition,
|
||||||
|
EndPosition: req.EndPosition,
|
||||||
|
Color: color,
|
||||||
|
Source: "web",
|
||||||
|
ModifiedAt: time.Now(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusCreated, result.Highlight)
|
||||||
|
}
|
||||||
|
|
||||||
highlight, err := mh.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
|
highlight, err := mh.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
|
||||||
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
MediaItemID: pgMediaID,
|
||||||
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
UserID: pgUserID,
|
||||||
SelectionText: req.SelectionText,
|
SelectionText: req.SelectionText,
|
||||||
StartPosition: pgtype.Text{String: req.StartPosition, Valid: true},
|
StartPosition: pgtype.Text{String: req.StartPosition, Valid: true},
|
||||||
EndPosition: pgtype.Text{String: req.EndPosition, Valid: true},
|
EndPosition: pgtype.Text{String: req.EndPosition, Valid: true},
|
||||||
@@ -1613,7 +1660,16 @@ func (mh *MediaHandler) DeleteMediaHighlight(c *echo.Context) error {
|
|||||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid highlight id"})
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid highlight id"})
|
||||||
}
|
}
|
||||||
|
|
||||||
err = mh.db.DeleteMediaHighlight(c.Request().Context(), pgtype.UUID{Bytes: highlightUUID, Valid: true})
|
pgHighlightID := pgtype.UUID{Bytes: highlightUUID, Valid: true}
|
||||||
|
|
||||||
|
if mh.annotationSvc != nil {
|
||||||
|
if err := mh.annotationSvc.TombstoneHighlightByID(c.Request().Context(), pgHighlightID, "web"); err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
return c.NoContent(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mh.db.DeleteMediaHighlight(c.Request().Context(), pgHighlightID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ type Config struct {
|
|||||||
ConnManager *sync.ConnectionManager
|
ConnManager *sync.ConnectionManager
|
||||||
QueueProcessor *sync.SyncQueueProcessor
|
QueueProcessor *sync.SyncQueueProcessor
|
||||||
ProgressService *sync.ProgressService
|
ProgressService *sync.ProgressService
|
||||||
|
AnnotationService *sync.AnnotationService
|
||||||
DeviceAuthMiddleware *middleware.DeviceAuthMiddleware
|
DeviceAuthMiddleware *middleware.DeviceAuthMiddleware
|
||||||
LoginTracker *ratelimit.LoginAttemptTracker
|
LoginTracker *ratelimit.LoginAttemptTracker
|
||||||
ScannerHandler *handlers.Handler
|
ScannerHandler *handlers.Handler
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ func registerSyncRoutes(cfg *Config) {
|
|||||||
// API clients can use Authorization header: Authorization: Bearer {token}
|
// API clients can use Authorization header: Authorization: Bearer {token}
|
||||||
koboHandler := handlers.NewKoboHandler(cfg.Queries, cfg.ConnManager)
|
koboHandler := handlers.NewKoboHandler(cfg.Queries, cfg.ConnManager)
|
||||||
koboHandler.SetProgressService(cfg.ProgressService)
|
koboHandler.SetProgressService(cfg.ProgressService)
|
||||||
|
koboHandler.SetAnnotationService(cfg.AnnotationService)
|
||||||
koboHandler.SetLibraryService(cfg.LibraryService)
|
koboHandler.SetLibraryService(cfg.LibraryService)
|
||||||
koboSync := e.Group("/api/sync/kobo/:token")
|
koboSync := e.Group("/api/sync/kobo/:token")
|
||||||
koboSync.POST("/markup", cfg.DeviceAuthMiddleware.Authenticate(koboHandler.Markup))
|
koboSync.POST("/markup", cfg.DeviceAuthMiddleware.Authenticate(koboHandler.Markup))
|
||||||
|
|||||||
Reference in New Issue
Block a user