refactor(annotations): AnnotationService is a required constructor dependency
H1's doctrine applied to the annotation service, which had been left on setters: NewMediaHandler/NewKoboHandler/NewKOReaderHandler/ NewSyncQueueProcessor(WithConfig) now take it as a parameter; the four SetAnnotationService setters are deleted; wiring updated (main.go, router/sync.go, test harness). With the service unskippable, the 18 nil-guards and their plain-query fallbacks die: - media.go: the create fallbacks for notes/highlights/bookmarks (the bookmark one was the H3 census's Bug #1 — it dropped percentage/chapter_reference the sync path carries), the highlight PUT fallback, and the three delete-path plain deletes (tombstone is now the only delete semantics). - kobo.go: both markup-ingest annotation loops (the bookmark fallback wrote the text into media_notes — a category error the fallback never should have had). - koreader.go: all three push-stack guards. - queue.go: the three "annotation service not available" errors in syncNote/syncHighlight/syncBookmark. Behavior note: the deleted highlight create/PUT fallbacks were the only paths passing note_id to the plain query — the sync service has no NoteID field, so the web's highlight→note link was already being dropped on the live path; this commit makes that pre-existing limitation explicit rather than papering it over (surfacing note_id on SaveHighlightRequest is left for a behavior commit that needs it). Pins (now 20 incl. the 3 annotation CRUD suites) + H1 tests + both unit suites green.
This commit is contained in:
1 parent
ff029f1ad5
commit
c579179fee
9 files changed
+290
-564
No files matched your search
+3
-6
@@ -130,8 +130,7 @@ func main() {
|
|||||||
maintenanceCancel := annotationService.StartDailyMaintenance()
|
maintenanceCancel := annotationService.StartDailyMaintenance()
|
||||||
defer maintenanceCancel()
|
defer maintenanceCancel()
|
||||||
|
|
||||||
queueProcessor := sync.NewSyncQueueProcessorWithConfig(queries, progressService, registry.SyncQueueConfig().Interval, registry.SyncQueueConfig().BatchSize)
|
queueProcessor := sync.NewSyncQueueProcessorWithConfig(queries, progressService, annotationService, registry.SyncQueueConfig().Interval, registry.SyncQueueConfig().BatchSize)
|
||||||
queueProcessor.SetAnnotationService(annotationService)
|
|
||||||
|
|
||||||
// Create library service
|
// Create library service
|
||||||
libraryService := services.NewLibraryService(queries)
|
libraryService := services.NewLibraryService(queries)
|
||||||
@@ -147,8 +146,7 @@ func main() {
|
|||||||
worker := services.NewWorkerWithConfig(workerCfg.Size, workerCfg.QueueCap, connManager)
|
worker := services.NewWorkerWithConfig(workerCfg.Size, workerCfg.QueueCap, connManager)
|
||||||
services.WorkerInstance = worker
|
services.WorkerInstance = worker
|
||||||
|
|
||||||
koreaderHandler := handlers.NewKOReaderHandler(queries, connManager, queueProcessor, progressService)
|
koreaderHandler := handlers.NewKOReaderHandler(queries, connManager, queueProcessor, progressService, annotationService)
|
||||||
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, progressService)
|
conflictHandler := handlers.NewConflictHandler(queries, connManager, progressService)
|
||||||
@@ -167,8 +165,7 @@ func main() {
|
|||||||
dashboardHandler := handlers.NewDashboardHandler(queries)
|
dashboardHandler := handlers.NewDashboardHandler(queries)
|
||||||
seriesHandler := handlers.NewSeriesHandler(queries)
|
seriesHandler := handlers.NewSeriesHandler(queries)
|
||||||
filtersHandler := handlers.NewFiltersHandler(queries)
|
filtersHandler := handlers.NewFiltersHandler(queries)
|
||||||
mediaHandler := handlers.NewMediaHandler(queries, libraryService, progressService)
|
mediaHandler := handlers.NewMediaHandler(queries, libraryService, progressService, annotationService)
|
||||||
mediaHandler.SetAnnotationService(annotationService)
|
|
||||||
matchingHandler := handlers.NewMatchingHandler(queries, connManager)
|
matchingHandler := handlers.NewMatchingHandler(queries, connManager)
|
||||||
jobsHandler := handlers.NewJobsHandler(queries, worker)
|
jobsHandler := handlers.NewJobsHandler(queries, worker)
|
||||||
|
|
||||||
|
|||||||
@@ -483,12 +483,11 @@ func setupTestServer(t *testing.T) *TestServerSetup {
|
|||||||
progressService := wsync.NewProgressService(queries, connManager)
|
progressService := wsync.NewProgressService(queries, connManager)
|
||||||
annotationService := wsync.NewAnnotationService(queries, connManager)
|
annotationService := wsync.NewAnnotationService(queries, connManager)
|
||||||
|
|
||||||
queueProcessor := wsync.NewSyncQueueProcessor(queries, progressService)
|
queueProcessor := wsync.NewSyncQueueProcessor(queries, progressService, annotationService)
|
||||||
queueCtx, queueCancel := context.WithCancel(context.Background())
|
queueCtx, queueCancel := context.WithCancel(context.Background())
|
||||||
go queueProcessor.Start(queueCtx)
|
go queueProcessor.Start(queueCtx)
|
||||||
|
|
||||||
koreaderHandler := handlers.NewKOReaderHandler(queries, connManager, queueProcessor, progressService)
|
koreaderHandler := handlers.NewKOReaderHandler(queries, connManager, queueProcessor, progressService, annotationService)
|
||||||
koreaderHandler.SetAnnotationService(annotationService)
|
|
||||||
wsHandler := handlers.NewWSHandler(queries, connManager, cfg.JWTSecret, deviceAuthMiddleware)
|
wsHandler := handlers.NewWSHandler(queries, connManager, cfg.JWTSecret, deviceAuthMiddleware)
|
||||||
conflictHandler := handlers.NewConflictHandler(queries, connManager, progressService)
|
conflictHandler := handlers.NewConflictHandler(queries, connManager, progressService)
|
||||||
analyticsHandler := handlers.NewAnalyticsHandler(queries)
|
analyticsHandler := handlers.NewAnalyticsHandler(queries)
|
||||||
@@ -508,8 +507,7 @@ func setupTestServer(t *testing.T) *TestServerSetup {
|
|||||||
statisticsService := services.NewStatisticsService(queries)
|
statisticsService := services.NewStatisticsService(queries)
|
||||||
statsHandler := handlers.NewStatsHandler(statisticsService)
|
statsHandler := handlers.NewStatsHandler(statisticsService)
|
||||||
seriesHandler := handlers.NewSeriesHandler(queries)
|
seriesHandler := handlers.NewSeriesHandler(queries)
|
||||||
mediaHandler := handlers.NewMediaHandler(queries, libraryService, progressService, worker)
|
mediaHandler := handlers.NewMediaHandler(queries, libraryService, progressService, annotationService, worker)
|
||||||
mediaHandler.SetAnnotationService(annotationService)
|
|
||||||
matchingHandler := handlers.NewMatchingHandler(queries, connManager)
|
matchingHandler := handlers.NewMatchingHandler(queries, connManager)
|
||||||
|
|
||||||
// Create conversion service for OPDS
|
// Create conversion service for OPDS
|
||||||
|
|||||||
+88
-170
@@ -26,12 +26,8 @@ type KoboHandler struct {
|
|||||||
bookResolver *services.BookResolver
|
bookResolver *services.BookResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewKoboHandler(db *database.Queries, connManager *wsync.ConnectionManager, progressSvc *wsync.ProgressService) *KoboHandler {
|
func NewKoboHandler(db *database.Queries, connManager *wsync.ConnectionManager, progressSvc *wsync.ProgressService, annotationSvc *wsync.AnnotationService) *KoboHandler {
|
||||||
return &KoboHandler{db: db, connManager: connManager, progressSvc: progressSvc, bookResolver: services.NewBookResolver(db)}
|
return &KoboHandler{db: db, connManager: connManager, progressSvc: progressSvc, annotationSvc: annotationSvc, bookResolver: services.NewBookResolver(db)}
|
||||||
}
|
|
||||||
|
|
||||||
func (h *KoboHandler) SetAnnotationService(svc *wsync.AnnotationService) {
|
|
||||||
h.annotationSvc = svc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *KoboHandler) SetLibraryService(svc LibraryPathResolver) {
|
func (h *KoboHandler) SetLibraryService(svc LibraryPathResolver) {
|
||||||
@@ -475,65 +471,43 @@ func (h *KoboHandler) Markup(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{}{
|
||||||
deviceData, _ := json.Marshal(map[string]interface{}{
|
"bookmark_id": bookmarkSync.BookmarkId,
|
||||||
"bookmark_id": bookmarkSync.BookmarkId,
|
"date_created": bookmarkSync.DateCreated,
|
||||||
"date_created": bookmarkSync.DateCreated,
|
})
|
||||||
})
|
|
||||||
|
|
||||||
result, err := h.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
|
result, err := h.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
|
||||||
MediaItemID: pgMediaUUID,
|
MediaItemID: pgMediaUUID,
|
||||||
UserID: pgUserID,
|
UserID: pgUserID,
|
||||||
SelectionText: bookmarkSync.BookmarkText,
|
SelectionText: bookmarkSync.BookmarkText,
|
||||||
StartPosition: bookmarkSync.BookmarkId,
|
StartPosition: bookmarkSync.BookmarkId,
|
||||||
EndPosition: bookmarkSync.BookmarkId,
|
EndPosition: bookmarkSync.BookmarkId,
|
||||||
Color: "#ffff00",
|
Color: "#ffff00",
|
||||||
NoteText: bookmarkSync.BookmarkTitle,
|
NoteText: bookmarkSync.BookmarkTitle,
|
||||||
Source: "kobo",
|
Source: "kobo",
|
||||||
DeviceSyncData: deviceData,
|
DeviceSyncData: deviceData,
|
||||||
})
|
})
|
||||||
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
||||||
bookmarksSynced++
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
h.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
|
|
||||||
MediaItemID: pgMediaUUID,
|
|
||||||
UserID: pgUserID,
|
|
||||||
SelectionText: bookmarkSync.BookmarkText,
|
|
||||||
StartPosition: pgtype.Text{String: bookmarkSync.BookmarkId, Valid: true},
|
|
||||||
EndPosition: pgtype.Text{String: bookmarkSync.BookmarkId, Valid: true},
|
|
||||||
Color: pgtype.Text{String: "#ffff00", Valid: true},
|
|
||||||
})
|
|
||||||
bookmarksSynced++
|
bookmarksSynced++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "bookmark":
|
case "bookmark":
|
||||||
if bookmarkSync.BookmarkText != "" {
|
if bookmarkSync.BookmarkText != "" {
|
||||||
if h.annotationSvc != nil {
|
deviceData, _ := json.Marshal(map[string]interface{}{
|
||||||
deviceData, _ := json.Marshal(map[string]interface{}{
|
"bookmark_id": bookmarkSync.BookmarkId,
|
||||||
"bookmark_id": bookmarkSync.BookmarkId,
|
"date_created": bookmarkSync.DateCreated,
|
||||||
"date_created": bookmarkSync.DateCreated,
|
})
|
||||||
})
|
|
||||||
|
|
||||||
result, err := h.annotationSvc.SaveBookmark(c.Request().Context(), wsync.SaveBookmarkRequest{
|
result, err := h.annotationSvc.SaveBookmark(c.Request().Context(), wsync.SaveBookmarkRequest{
|
||||||
MediaItemID: pgMediaUUID,
|
MediaItemID: pgMediaUUID,
|
||||||
UserID: pgUserID,
|
UserID: pgUserID,
|
||||||
Title: bookmarkSync.BookmarkText,
|
Title: bookmarkSync.BookmarkText,
|
||||||
Position: bookmarkSync.BookmarkId,
|
Position: bookmarkSync.BookmarkId,
|
||||||
ChapterNumber: int32(bookmarkSync.Chapter),
|
ChapterNumber: int32(bookmarkSync.Chapter),
|
||||||
Source: "kobo",
|
Source: "kobo",
|
||||||
DeviceSyncData: deviceData,
|
DeviceSyncData: deviceData,
|
||||||
})
|
})
|
||||||
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
||||||
bookmarksSynced++
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
h.db.CreateMediaNote(c.Request().Context(), database.CreateMediaNoteParams{
|
|
||||||
MediaItemID: pgMediaUUID,
|
|
||||||
UserID: pgUserID,
|
|
||||||
Content: bookmarkSync.BookmarkText,
|
|
||||||
Position: pgtype.Text{String: bookmarkSync.BookmarkId, Valid: true},
|
|
||||||
})
|
|
||||||
bookmarksSynced++
|
bookmarksSynced++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -600,7 +574,7 @@ func (h *KoboHandler) Markup(c *echo.Context) error {
|
|||||||
BookmarksSynced: bookmarksSynced,
|
BookmarksSynced: bookmarksSynced,
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.annotationSvc != nil && len(processedBooks) > 0 {
|
if len(processedBooks) > 0 {
|
||||||
cutoff := pgtype.Timestamptz{Time: time.Now().Add(-h.annotationSvc.ActiveTombstoneTTL()), Valid: true}
|
cutoff := pgtype.Timestamptz{Time: time.Now().Add(-h.annotationSvc.ActiveTombstoneTTL()), Valid: true}
|
||||||
for mediaItemID, contentId := range processedBooks {
|
for mediaItemID, contentId := range processedBooks {
|
||||||
tombstones, _ := h.db.GetTombstonedAnnotationsForBook(c.Request().Context(), database.GetTombstonedAnnotationsForBookParams{
|
tombstones, _ := h.db.GetTombstonedAnnotationsForBook(c.Request().Context(), database.GetTombstonedAnnotationsForBookParams{
|
||||||
@@ -668,65 +642,43 @@ 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{}{
|
||||||
deviceData, _ := json.Marshal(map[string]interface{}{
|
"bookmark_id": bookmarkSync.BookmarkId,
|
||||||
"bookmark_id": bookmarkSync.BookmarkId,
|
"date_created": bookmarkSync.DateCreated,
|
||||||
"date_created": bookmarkSync.DateCreated,
|
})
|
||||||
})
|
|
||||||
|
|
||||||
result, err := h.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
|
result, err := h.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
|
||||||
MediaItemID: pgMediaUUID,
|
MediaItemID: pgMediaUUID,
|
||||||
UserID: pgUserID,
|
UserID: pgUserID,
|
||||||
SelectionText: bookmarkSync.BookmarkText,
|
SelectionText: bookmarkSync.BookmarkText,
|
||||||
StartPosition: bookmarkSync.BookmarkId,
|
StartPosition: bookmarkSync.BookmarkId,
|
||||||
EndPosition: bookmarkSync.BookmarkId,
|
EndPosition: bookmarkSync.BookmarkId,
|
||||||
Color: "#ffff00",
|
Color: "#ffff00",
|
||||||
NoteText: bookmarkSync.BookmarkTitle,
|
NoteText: bookmarkSync.BookmarkTitle,
|
||||||
Source: "kobo",
|
Source: "kobo",
|
||||||
DeviceSyncData: deviceData,
|
DeviceSyncData: deviceData,
|
||||||
})
|
})
|
||||||
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
||||||
bookmarksSynced++
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
h.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
|
|
||||||
MediaItemID: pgMediaUUID,
|
|
||||||
UserID: pgUserID,
|
|
||||||
SelectionText: bookmarkSync.BookmarkText,
|
|
||||||
StartPosition: pgtype.Text{String: bookmarkSync.BookmarkId, Valid: true},
|
|
||||||
EndPosition: pgtype.Text{String: bookmarkSync.BookmarkId, Valid: true},
|
|
||||||
Color: pgtype.Text{String: "#ffff00", Valid: true},
|
|
||||||
})
|
|
||||||
bookmarksSynced++
|
bookmarksSynced++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "bookmark":
|
case "bookmark":
|
||||||
if bookmarkSync.BookmarkText != "" {
|
if bookmarkSync.BookmarkText != "" {
|
||||||
if h.annotationSvc != nil {
|
deviceData, _ := json.Marshal(map[string]interface{}{
|
||||||
deviceData, _ := json.Marshal(map[string]interface{}{
|
"bookmark_id": bookmarkSync.BookmarkId,
|
||||||
"bookmark_id": bookmarkSync.BookmarkId,
|
"date_created": bookmarkSync.DateCreated,
|
||||||
"date_created": bookmarkSync.DateCreated,
|
})
|
||||||
})
|
|
||||||
|
|
||||||
result, err := h.annotationSvc.SaveBookmark(c.Request().Context(), wsync.SaveBookmarkRequest{
|
result, err := h.annotationSvc.SaveBookmark(c.Request().Context(), wsync.SaveBookmarkRequest{
|
||||||
MediaItemID: pgMediaUUID,
|
MediaItemID: pgMediaUUID,
|
||||||
UserID: pgUserID,
|
UserID: pgUserID,
|
||||||
Title: bookmarkSync.BookmarkText,
|
Title: bookmarkSync.BookmarkText,
|
||||||
Position: bookmarkSync.BookmarkId,
|
Position: bookmarkSync.BookmarkId,
|
||||||
ChapterNumber: int32(bookmarkSync.Chapter),
|
ChapterNumber: int32(bookmarkSync.Chapter),
|
||||||
Source: "kobo",
|
Source: "kobo",
|
||||||
DeviceSyncData: deviceData,
|
DeviceSyncData: deviceData,
|
||||||
})
|
})
|
||||||
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
||||||
bookmarksSynced++
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
h.db.CreateMediaNote(c.Request().Context(), database.CreateMediaNoteParams{
|
|
||||||
MediaItemID: pgMediaUUID,
|
|
||||||
UserID: pgUserID,
|
|
||||||
Content: bookmarkSync.BookmarkText,
|
|
||||||
Position: pgtype.Text{String: bookmarkSync.BookmarkId, Valid: true},
|
|
||||||
})
|
|
||||||
bookmarksSynced++
|
bookmarksSynced++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -842,77 +794,43 @@ 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{
|
||||||
result, err := h.annotationSvc.SaveBookmark(c.Request().Context(), wsync.SaveBookmarkRequest{
|
MediaItemID: pgMediaUUID,
|
||||||
MediaItemID: pgMediaUUID,
|
UserID: pgUserID,
|
||||||
UserID: pgUserID,
|
Title: bookmark.BookmarkText,
|
||||||
Title: bookmark.BookmarkText,
|
Position: bookmark.BookmarkId,
|
||||||
Position: bookmark.BookmarkId,
|
Source: "kobo",
|
||||||
Source: "kobo",
|
})
|
||||||
})
|
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
||||||
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
|
||||||
bookmarksSent++
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
h.db.CreateMediaNote(c.Request().Context(), database.CreateMediaNoteParams{
|
|
||||||
MediaItemID: pgMediaUUID,
|
|
||||||
UserID: pgUserID,
|
|
||||||
Content: bookmark.BookmarkText,
|
|
||||||
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{
|
|
||||||
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 {
|
|
||||||
if h.annotationSvc != nil {
|
|
||||||
result, err := h.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
|
result, err := h.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
|
||||||
MediaItemID: pgMediaUUID,
|
MediaItemID: pgMediaUUID,
|
||||||
UserID: pgUserID,
|
UserID: pgUserID,
|
||||||
SelectionText: highlight.BookmarkText,
|
SelectionText: bookmark.BookmarkText,
|
||||||
StartPosition: highlight.BookmarkId,
|
StartPosition: bookmark.BookmarkId,
|
||||||
EndPosition: highlight.BookmarkId,
|
EndPosition: bookmark.BookmarkId,
|
||||||
Color: "#ffff00",
|
Color: "#ffff00",
|
||||||
Source: "kobo",
|
Source: "kobo",
|
||||||
})
|
})
|
||||||
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
||||||
highlightsSent++
|
highlightsSent++
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
h.db.CreateMediaHighlight(c.Request().Context(), database.CreateMediaHighlightParams{
|
}
|
||||||
MediaItemID: pgMediaUUID,
|
|
||||||
UserID: pgUserID,
|
for _, highlight := range syncData.Highlights {
|
||||||
SelectionText: highlight.BookmarkText,
|
result, err := h.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
|
||||||
StartPosition: pgtype.Text{String: highlight.BookmarkId, Valid: true},
|
MediaItemID: pgMediaUUID,
|
||||||
EndPosition: pgtype.Text{String: highlight.BookmarkId, Valid: true},
|
UserID: pgUserID,
|
||||||
Color: pgtype.Text{String: "#ffff00", Valid: true},
|
SelectionText: highlight.BookmarkText,
|
||||||
})
|
StartPosition: highlight.BookmarkId,
|
||||||
|
EndPosition: highlight.BookmarkId,
|
||||||
|
Color: "#ffff00",
|
||||||
|
Source: "kobo",
|
||||||
|
})
|
||||||
|
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
||||||
highlightsSent++
|
highlightsSent++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+78
-122
@@ -36,20 +36,17 @@ type LibraryPathResolver interface {
|
|||||||
ResolveMediaPath(ctx context.Context, libraryID pgtype.UUID, relativePath string) (string, error)
|
ResolveMediaPath(ctx context.Context, libraryID pgtype.UUID, relativePath string) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewKOReaderHandler(db *database.Queries, connManager *wsync.ConnectionManager, queue *wsync.SyncQueueProcessor, progressSvc *wsync.ProgressService) *KOReaderHandler {
|
func NewKOReaderHandler(db *database.Queries, connManager *wsync.ConnectionManager, queue *wsync.SyncQueueProcessor, progressSvc *wsync.ProgressService, annotationSvc *wsync.AnnotationService) *KOReaderHandler {
|
||||||
return &KOReaderHandler{
|
return &KOReaderHandler{
|
||||||
db: db,
|
db: db,
|
||||||
connManager: connManager,
|
connManager: connManager,
|
||||||
queue: queue,
|
queue: queue,
|
||||||
progressSvc: progressSvc,
|
progressSvc: progressSvc,
|
||||||
bookResolver: services.NewBookResolver(db),
|
annotationSvc: annotationSvc,
|
||||||
|
bookResolver: services.NewBookResolver(db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *KOReaderHandler) SetAnnotationService(svc *wsync.AnnotationService) {
|
|
||||||
h.annotationSvc = svc
|
|
||||||
}
|
|
||||||
|
|
||||||
// annotationEpub carries the per-book context every locator conversion
|
// annotationEpub carries the per-book context every locator conversion
|
||||||
// needs: the media item (format gating) and the resolved EPUB path. It is
|
// needs: the media item (format gating) and the resolved EPUB path. It is
|
||||||
// resolved once per request so all annotations in a push share one
|
// resolved once per request so all annotations in a push share one
|
||||||
@@ -1489,50 +1486,37 @@ 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 {
|
ec := h.loadAnnotationEpub(ctx, mediaItemID)
|
||||||
ec := h.loadAnnotationEpub(ctx, mediaItemID)
|
|
||||||
|
|
||||||
pctLoc := 0.0
|
pctLoc := 0.0
|
||||||
if bookmark.Percentage != nil {
|
if bookmark.Percentage != nil {
|
||||||
pctLoc = *bookmark.Percentage
|
pctLoc = *bookmark.Percentage
|
||||||
}
|
}
|
||||||
if pctLoc == 0 {
|
if pctLoc == 0 {
|
||||||
pctLoc = h.deriveAnnotationPercentage(ec, position, int(bookmark.Page))
|
pctLoc = h.deriveAnnotationPercentage(ec, position, int(bookmark.Page))
|
||||||
}
|
}
|
||||||
cfiPosition := h.convertBookmarkPosition(ec, position, pctLoc)
|
cfiPosition := h.convertBookmarkPosition(ec, position, pctLoc)
|
||||||
|
|
||||||
deviceData, _ := json.Marshal(map[string]interface{}{
|
deviceData, _ := json.Marshal(map[string]interface{}{
|
||||||
"datetime": bookmark.Datetime,
|
"datetime": bookmark.Datetime,
|
||||||
"pos0": bookmark.Pos0,
|
"pos0": bookmark.Pos0,
|
||||||
"page": bookmark.Page,
|
"page": bookmark.Page,
|
||||||
})
|
})
|
||||||
|
|
||||||
result, err := h.annotationSvc.SaveBookmark(ctx, wsync.SaveBookmarkRequest{
|
result, err := h.annotationSvc.SaveBookmark(ctx, wsync.SaveBookmarkRequest{
|
||||||
MediaItemID: mediaItemID,
|
MediaItemID: mediaItemID,
|
||||||
UserID: pgUserID,
|
UserID: pgUserID,
|
||||||
Title: bookmark.Text,
|
Title: bookmark.Text,
|
||||||
Position: position,
|
Position: position,
|
||||||
CFIPosition: cfiPosition,
|
CFIPosition: cfiPosition,
|
||||||
PercentageLoc: pctLoc,
|
PercentageLoc: pctLoc,
|
||||||
ChapterNumber: int32(bookmark.Chapter),
|
ChapterNumber: int32(bookmark.Chapter),
|
||||||
Source: "koreader",
|
Source: "koreader",
|
||||||
OriginSource: "koreader",
|
OriginSource: "koreader",
|
||||||
DeviceSyncData: deviceData,
|
DeviceSyncData: deviceData,
|
||||||
})
|
})
|
||||||
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
||||||
bookmarksSynced++
|
bookmarksSynced++
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_, err := h.db.CreateMediaNote(ctx, database.CreateMediaNoteParams{
|
|
||||||
MediaItemID: mediaItemID,
|
|
||||||
UserID: pgUserID,
|
|
||||||
Content: bookmark.Text,
|
|
||||||
Position: pgtype.Text{String: position, Valid: position != ""},
|
|
||||||
})
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
bookmarksSynced++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1554,35 +1538,22 @@ 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{}{
|
||||||
deviceData, _ := json.Marshal(map[string]interface{}{
|
"datetime": note.Datetime,
|
||||||
"datetime": note.Datetime,
|
"pos0": note.Pos0,
|
||||||
"pos0": note.Pos0,
|
"page": note.Page,
|
||||||
"page": note.Page,
|
})
|
||||||
})
|
|
||||||
|
|
||||||
result, err := h.annotationSvc.SaveNote(ctx, wsync.SaveNoteRequest{
|
result, err := h.annotationSvc.SaveNote(ctx, wsync.SaveNoteRequest{
|
||||||
MediaItemID: mediaItemID,
|
MediaItemID: mediaItemID,
|
||||||
UserID: pgUserID,
|
UserID: pgUserID,
|
||||||
Content: note.Notes,
|
Content: note.Notes,
|
||||||
Position: position,
|
Position: position,
|
||||||
Source: "koreader",
|
Source: "koreader",
|
||||||
DeviceSyncData: deviceData,
|
DeviceSyncData: deviceData,
|
||||||
})
|
})
|
||||||
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
||||||
notesSynced++
|
notesSynced++
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_, err := h.db.CreateMediaNote(ctx, database.CreateMediaNoteParams{
|
|
||||||
MediaItemID: mediaItemID,
|
|
||||||
UserID: pgUserID,
|
|
||||||
Content: note.Notes,
|
|
||||||
Position: pgtype.Text{String: position, Valid: position != ""},
|
|
||||||
})
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
notesSynced++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1609,53 +1580,38 @@ func (h *KOReaderHandler) SyncBookmarks(c *echo.Context) error {
|
|||||||
color = mapColorFromKOReader(highlight.Color)
|
color = mapColorFromKOReader(highlight.Color)
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.annotationSvc != nil {
|
ec := h.loadAnnotationEpub(ctx, mediaItemID)
|
||||||
ec := h.loadAnnotationEpub(ctx, mediaItemID)
|
|
||||||
|
|
||||||
pctStart := 0.0
|
pctStart := 0.0
|
||||||
if highlight.Percentage != nil {
|
if highlight.Percentage != nil {
|
||||||
pctStart = *highlight.Percentage
|
pctStart = *highlight.Percentage
|
||||||
}
|
}
|
||||||
|
|
||||||
epubcfiStart, epubcfiEnd := h.convertHighlightPositions(ec, highlight.Pos0, highlight.Pos1, highlight.Text, pctStart)
|
epubcfiStart, epubcfiEnd := h.convertHighlightPositions(ec, highlight.Pos0, highlight.Pos1, highlight.Text, pctStart)
|
||||||
|
|
||||||
deviceData, _ := json.Marshal(map[string]interface{}{
|
deviceData, _ := json.Marshal(map[string]interface{}{
|
||||||
"datetime": highlight.Datetime,
|
"datetime": highlight.Datetime,
|
||||||
"pos0": highlight.Pos0,
|
"pos0": highlight.Pos0,
|
||||||
"pos1": highlight.Pos1,
|
"pos1": highlight.Pos1,
|
||||||
"page": highlight.Page,
|
"page": highlight.Page,
|
||||||
})
|
})
|
||||||
|
|
||||||
result, err := h.annotationSvc.SaveHighlight(ctx, wsync.SaveHighlightRequest{
|
result, err := h.annotationSvc.SaveHighlight(ctx, wsync.SaveHighlightRequest{
|
||||||
MediaItemID: mediaItemID,
|
MediaItemID: mediaItemID,
|
||||||
UserID: pgUserID,
|
UserID: pgUserID,
|
||||||
SelectionText: highlight.Text,
|
SelectionText: highlight.Text,
|
||||||
StartPosition: startPos,
|
StartPosition: startPos,
|
||||||
EndPosition: endPos,
|
EndPosition: endPos,
|
||||||
Color: color,
|
Color: color,
|
||||||
NoteText: highlight.Notes,
|
NoteText: highlight.Notes,
|
||||||
PercentageStart: pctStart,
|
PercentageStart: pctStart,
|
||||||
EpubcfiStart: epubcfiStart,
|
EpubcfiStart: epubcfiStart,
|
||||||
EpubcfiEnd: epubcfiEnd,
|
EpubcfiEnd: epubcfiEnd,
|
||||||
Source: "koreader",
|
Source: "koreader",
|
||||||
DeviceSyncData: deviceData,
|
DeviceSyncData: deviceData,
|
||||||
})
|
})
|
||||||
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
if err == nil && result.Outcome != wsync.SaveOutcomeDeleted {
|
||||||
highlightsSynced++
|
highlightsSynced++
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_, err := h.db.CreateMediaHighlight(ctx, database.CreateMediaHighlightParams{
|
|
||||||
MediaItemID: mediaItemID,
|
|
||||||
UserID: pgUserID,
|
|
||||||
SelectionText: highlight.Text,
|
|
||||||
StartPosition: pgtype.Text{String: startPos, Valid: startPos != ""},
|
|
||||||
EndPosition: pgtype.Text{String: endPos, Valid: endPos != ""},
|
|
||||||
Color: pgtype.Text{String: color, Valid: true},
|
|
||||||
})
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
highlightsSynced++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+98
-225
@@ -183,12 +183,13 @@ type MediaHandler struct {
|
|||||||
// NewMediaHandler requires the shared ProgressService — progress writes
|
// NewMediaHandler requires the shared ProgressService — progress writes
|
||||||
// must funnel through SaveProgress (the LWW-seeded canonical writer);
|
// must funnel through SaveProgress (the LWW-seeded canonical writer);
|
||||||
// a nil service has nowhere to fall back to and must not be wired.
|
// a nil service has nowhere to fall back to and must not be wired.
|
||||||
func NewMediaHandler(db *database.Queries, libraryService *services.LibraryService, progressSvc *wsync.ProgressService, worker ...*services.Worker) *MediaHandler {
|
func NewMediaHandler(db *database.Queries, libraryService *services.LibraryService, progressSvc *wsync.ProgressService, annotationSvc *wsync.AnnotationService, worker ...*services.Worker) *MediaHandler {
|
||||||
mh := &MediaHandler{
|
mh := &MediaHandler{
|
||||||
db: db,
|
db: db,
|
||||||
libraryService: libraryService,
|
libraryService: libraryService,
|
||||||
searchService: services.NewSearchService(db),
|
searchService: services.NewSearchService(db),
|
||||||
progressSvc: progressSvc,
|
progressSvc: progressSvc,
|
||||||
|
annotationSvc: annotationSvc,
|
||||||
}
|
}
|
||||||
if len(worker) > 0 && worker[0] != nil {
|
if len(worker) > 0 && worker[0] != nil {
|
||||||
mh.worker = worker[0]
|
mh.worker = worker[0]
|
||||||
@@ -196,10 +197,6 @@ func NewMediaHandler(db *database.Queries, libraryService *services.LibraryServi
|
|||||||
return mh
|
return mh
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mh *MediaHandler) SetAnnotationService(svc *wsync.AnnotationService) {
|
|
||||||
mh.annotationSvc = svc
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecuteSearch performs search and returns results with count
|
// ExecuteSearch performs search and returns results with count
|
||||||
// Public wrapper for shared search logic used by both JSON and HTML endpoints
|
// Public wrapper for shared search logic used by both JSON and HTML endpoints
|
||||||
func (h *MediaHandler) ExecuteSearch(ctx context.Context, params services.SearchParams) ([]database.SearchMediaItemsUnifiedRow, int, error) {
|
func (h *MediaHandler) ExecuteSearch(ctx context.Context, params services.SearchParams) ([]database.SearchMediaItemsUnifiedRow, int, error) {
|
||||||
@@ -1197,22 +1194,22 @@ func (mh *MediaHandler) GetMediaReadingProgress(c *echo.Context) error {
|
|||||||
// additive keys per docs/contract_pins.md (H1).
|
// additive keys per docs/contract_pins.md (H1).
|
||||||
func progressResponse(progress database.GetUniversalProgressRow) map[string]interface{} {
|
func progressResponse(progress database.GetUniversalProgressRow) map[string]interface{} {
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"id": progress.ID,
|
"id": progress.ID,
|
||||||
"media_item_id": progress.MediaItemID,
|
"media_item_id": progress.MediaItemID,
|
||||||
"user_id": progress.UserID,
|
"user_id": progress.UserID,
|
||||||
"current_page": progress.CurrentPage,
|
"current_page": progress.CurrentPage,
|
||||||
"total_pages": progress.TotalPages,
|
"total_pages": progress.TotalPages,
|
||||||
"last_read_at": progress.LastReadAt,
|
"last_read_at": progress.LastReadAt,
|
||||||
"percentage": progress.Percentage,
|
"percentage": progress.Percentage,
|
||||||
"character_offset": progress.CharacterOffset,
|
"character_offset": progress.CharacterOffset,
|
||||||
"epubcfi": progress.Epubcfi,
|
"epubcfi": progress.Epubcfi,
|
||||||
"chapter": progress.Chapter,
|
"chapter": progress.Chapter,
|
||||||
"chapter_progress": progress.ChapterProgress,
|
"chapter_progress": progress.ChapterProgress,
|
||||||
"format_group": progress.FormatGroup,
|
"format_group": progress.FormatGroup,
|
||||||
"total_characters": progress.TotalCharacters,
|
"total_characters": progress.TotalCharacters,
|
||||||
"chapter_count": progress.ChapterCount,
|
"chapter_count": progress.ChapterCount,
|
||||||
"last_sync_device": progress.LastSyncDevice,
|
"last_sync_device": progress.LastSyncDevice,
|
||||||
"last_sync_source": progress.LastSyncSource,
|
"last_sync_source": progress.LastSyncSource,
|
||||||
"last_sync_timestamp": progress.LastSyncTimestamp,
|
"last_sync_timestamp": progress.LastSyncTimestamp,
|
||||||
// Row-carried fields surfaced additively (H1) — the web audiobook
|
// Row-carried fields surfaced additively (H1) — the web audiobook
|
||||||
// reader already reads audio_offset_seconds (audio.ts) and the
|
// reader already reads audio_offset_seconds (audio.ts) and the
|
||||||
@@ -1830,34 +1827,19 @@ 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()})
|
||||||
}
|
}
|
||||||
|
|
||||||
var note database.MediaNotes
|
result, err := mh.annotationSvc.SaveNote(c.Request().Context(), wsync.SaveNoteRequest{
|
||||||
if mh.annotationSvc != nil {
|
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
||||||
result, err := mh.annotationSvc.SaveNote(c.Request().Context(), wsync.SaveNoteRequest{
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
Content: req.Content,
|
||||||
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
Position: req.Position,
|
||||||
Content: req.Content,
|
Source: "web",
|
||||||
Position: req.Position,
|
ModifiedAt: time.Now(),
|
||||||
Source: "web",
|
})
|
||||||
ModifiedAt: time.Now(),
|
if err != nil {
|
||||||
})
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
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},
|
|
||||||
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
|
||||||
Content: req.Content,
|
|
||||||
Position: pgtype.Text{String: req.Position, Valid: req.Position != ""},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(http.StatusCreated, note)
|
return c.JSON(http.StatusCreated, result.Note)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMediaNote handles GET /api/media-items/:id/notes/:noteId
|
// GetMediaNote handles GET /api/media-items/:id/notes/:noteId
|
||||||
@@ -1928,15 +1910,7 @@ func (mh *MediaHandler) DeleteMediaNote(c *echo.Context) error {
|
|||||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
|
||||||
}
|
}
|
||||||
|
|
||||||
if mh.annotationSvc != nil {
|
if err := mh.annotationSvc.TombstoneNoteByID(c.Request().Context(), pgtype.UUID{Bytes: noteUUID, Valid: true}, pgtype.UUID{Bytes: userUUID, Valid: true}); err != nil {
|
||||||
err = mh.annotationSvc.TombstoneNoteByID(c.Request().Context(), pgtype.UUID{Bytes: noteUUID, Valid: true}, pgtype.UUID{Bytes: userUUID, Valid: true})
|
|
||||||
} else {
|
|
||||||
err = mh.db.DeleteMediaNote(c.Request().Context(), database.DeleteMediaNoteParams{
|
|
||||||
ID: pgtype.UUID{Bytes: noteUUID, Valid: true},
|
|
||||||
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
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()})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1990,15 +1964,6 @@ func (mh *MediaHandler) CreateMediaHighlight(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()})
|
||||||
}
|
}
|
||||||
|
|
||||||
var noteUUID pgtype.UUID
|
|
||||||
if req.NoteID != "" {
|
|
||||||
if noteID, err := uuid.Parse(req.NoteID); err != nil {
|
|
||||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
|
|
||||||
} else {
|
|
||||||
noteUUID = pgtype.UUID{Bytes: noteID, Valid: true}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
color := "#ffff00"
|
color := "#ffff00"
|
||||||
if req.Color != "" {
|
if req.Color != "" {
|
||||||
color = req.Color
|
color = req.Color
|
||||||
@@ -2007,43 +1972,26 @@ func (mh *MediaHandler) CreateMediaHighlight(c *echo.Context) error {
|
|||||||
pgMediaID := pgtype.UUID{Bytes: mediaUUID, Valid: true}
|
pgMediaID := pgtype.UUID{Bytes: mediaUUID, Valid: true}
|
||||||
pgUserID := pgtype.UUID{Bytes: userUUID, Valid: true}
|
pgUserID := pgtype.UUID{Bytes: userUUID, Valid: true}
|
||||||
|
|
||||||
if mh.annotationSvc != nil {
|
result, err := mh.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
|
||||||
result, err := mh.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
|
MediaItemID: pgMediaID,
|
||||||
MediaItemID: pgMediaID,
|
UserID: pgUserID,
|
||||||
UserID: pgUserID,
|
SelectionText: req.SelectionText,
|
||||||
SelectionText: req.SelectionText,
|
StartPosition: req.StartPosition,
|
||||||
StartPosition: req.StartPosition,
|
EndPosition: req.EndPosition,
|
||||||
EndPosition: req.EndPosition,
|
EpubcfiStart: req.EpubcfiStart,
|
||||||
EpubcfiStart: req.EpubcfiStart,
|
EpubcfiEnd: req.EpubcfiEnd,
|
||||||
EpubcfiEnd: req.EpubcfiEnd,
|
Color: color,
|
||||||
Color: color,
|
NoteText: req.NoteText,
|
||||||
NoteText: req.NoteText,
|
PercentageStart: req.PercentageStart,
|
||||||
PercentageStart: req.PercentageStart,
|
PercentageEnd: req.PercentageEnd,
|
||||||
PercentageEnd: req.PercentageEnd,
|
ChapterReference: req.ChapterReference,
|
||||||
ChapterReference: req.ChapterReference,
|
Source: "web",
|
||||||
Source: "web",
|
ModifiedAt: time.Now(),
|
||||||
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{
|
|
||||||
MediaItemID: pgMediaID,
|
|
||||||
UserID: pgUserID,
|
|
||||||
SelectionText: req.SelectionText,
|
|
||||||
StartPosition: pgtype.Text{String: req.StartPosition, Valid: true},
|
|
||||||
EndPosition: pgtype.Text{String: req.EndPosition, Valid: true},
|
|
||||||
Color: pgtype.Text{String: color, Valid: true},
|
|
||||||
NoteID: noteUUID,
|
|
||||||
})
|
})
|
||||||
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, result.Highlight)
|
||||||
return c.JSON(http.StatusCreated, highlight)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMediaHighlight handles GET /api/media-items/:id/highlights/:highlightId
|
// GetMediaHighlight handles GET /api/media-items/:id/highlights/:highlightId
|
||||||
@@ -2081,81 +2029,46 @@ func (mh *MediaHandler) UpdateMediaHighlight(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()})
|
||||||
}
|
}
|
||||||
|
|
||||||
var noteUUID pgtype.UUID
|
|
||||||
if req.NoteID != "" {
|
|
||||||
if noteID, err := uuid.Parse(req.NoteID); err != nil {
|
|
||||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid note id"})
|
|
||||||
} else {
|
|
||||||
noteUUID = pgtype.UUID{Bytes: noteID, Valid: true}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
color := "#ffff00"
|
color := "#ffff00"
|
||||||
if req.Color != "" {
|
if req.Color != "" {
|
||||||
color = req.Color
|
color = req.Color
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefer the sync-aware path: the same selection text + CFI resolves to
|
|
||||||
// the same dedup key, so this performs an LWW update of the existing row
|
|
||||||
// (including note_text and CFI columns the plain query cannot touch).
|
|
||||||
if mh.annotationSvc != nil {
|
|
||||||
userID := c.Get("user_id").(string)
|
|
||||||
userUUID, err := uuid.Parse(userID)
|
|
||||||
if err != nil {
|
|
||||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
|
|
||||||
}
|
|
||||||
mediaID := c.Param("id")
|
|
||||||
mediaUUID, err := uuid.Parse(mediaID)
|
|
||||||
if err != nil {
|
|
||||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
|
|
||||||
}
|
|
||||||
result, err := mh.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
|
|
||||||
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
|
||||||
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
|
||||||
SelectionText: req.SelectionText,
|
|
||||||
StartPosition: req.StartPosition,
|
|
||||||
EndPosition: req.EndPosition,
|
|
||||||
EpubcfiStart: req.EpubcfiStart,
|
|
||||||
EpubcfiEnd: req.EpubcfiEnd,
|
|
||||||
Color: color,
|
|
||||||
NoteText: req.NoteText,
|
|
||||||
PercentageStart: req.PercentageStart,
|
|
||||||
PercentageEnd: req.PercentageEnd,
|
|
||||||
ChapterReference: req.ChapterReference,
|
|
||||||
Source: "web",
|
|
||||||
ModifiedAt: time.Now(),
|
|
||||||
// The PUT targets this exact row (from the URL): identity must
|
|
||||||
// not be re-derived from content — a device echo has usually
|
|
||||||
// rewritten the stored CFI to point shape, so the computed key
|
|
||||||
// would miss and mint a duplicate beside the edited row.
|
|
||||||
HighlightID: pgtype.UUID{Bytes: highlightUUID, Valid: true},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, result.Highlight)
|
|
||||||
}
|
|
||||||
|
|
||||||
userID := c.Get("user_id").(string)
|
userID := c.Get("user_id").(string)
|
||||||
userUUID, err := uuid.Parse(userID)
|
userUUID, err := uuid.Parse(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid user"})
|
||||||
}
|
}
|
||||||
|
mediaID := c.Param("id")
|
||||||
highlight, err := mh.db.UpdateMediaHighlight(c.Request().Context(), database.UpdateMediaHighlightParams{
|
mediaUUID, err := uuid.Parse(mediaID)
|
||||||
ID: pgtype.UUID{Bytes: highlightUUID, Valid: true},
|
if err != nil {
|
||||||
SelectionText: req.SelectionText,
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
|
||||||
StartPosition: pgtype.Text{String: req.StartPosition, Valid: true},
|
}
|
||||||
EndPosition: pgtype.Text{String: req.EndPosition, Valid: true},
|
result, err := mh.annotationSvc.SaveHighlight(c.Request().Context(), wsync.SaveHighlightRequest{
|
||||||
Color: pgtype.Text{String: color, Valid: true},
|
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
||||||
NoteID: noteUUID,
|
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
||||||
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
SelectionText: req.SelectionText,
|
||||||
|
StartPosition: req.StartPosition,
|
||||||
|
EndPosition: req.EndPosition,
|
||||||
|
EpubcfiStart: req.EpubcfiStart,
|
||||||
|
EpubcfiEnd: req.EpubcfiEnd,
|
||||||
|
Color: color,
|
||||||
|
NoteText: req.NoteText,
|
||||||
|
PercentageStart: req.PercentageStart,
|
||||||
|
PercentageEnd: req.PercentageEnd,
|
||||||
|
ChapterReference: req.ChapterReference,
|
||||||
|
Source: "web",
|
||||||
|
ModifiedAt: time.Now(),
|
||||||
|
// The PUT targets this exact row (from the URL): identity must
|
||||||
|
// not be re-derived from content — a device echo has usually
|
||||||
|
// rewritten the stored CFI to point shape, so the computed key
|
||||||
|
// would miss and mint a duplicate beside the edited row.
|
||||||
|
HighlightID: pgtype.UUID{Bytes: highlightUUID, 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()})
|
||||||
}
|
}
|
||||||
|
return c.JSON(http.StatusOK, result.Highlight)
|
||||||
return c.JSON(http.StatusOK, highlight)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteMediaHighlight handles DELETE /api/media-items/:id/highlights/:highlightId
|
// DeleteMediaHighlight handles DELETE /api/media-items/:id/highlights/:highlightId
|
||||||
@@ -2175,24 +2088,12 @@ func (mh *MediaHandler) DeleteMediaHighlight(c *echo.Context) error {
|
|||||||
}
|
}
|
||||||
pgUserID := pgtype.UUID{Bytes: userUUID, Valid: true}
|
pgUserID := pgtype.UUID{Bytes: userUUID, Valid: true}
|
||||||
|
|
||||||
if mh.annotationSvc != nil {
|
if err := mh.annotationSvc.TombstoneHighlightByID(c.Request().Context(), pgUserID, pgHighlightID, "web"); err != nil {
|
||||||
if err := mh.annotationSvc.TombstoneHighlightByID(c.Request().Context(), pgUserID, pgHighlightID, "web"); err != nil {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "highlight not found"})
|
||||||
return c.JSON(http.StatusNotFound, map[string]string{"error": "highlight not found"})
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
||||||
}
|
}
|
||||||
return c.NoContent(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = mh.db.DeleteMediaHighlight(c.Request().Context(), database.DeleteMediaHighlightParams{
|
|
||||||
ID: pgHighlightID,
|
|
||||||
UserID: pgUserID,
|
|
||||||
})
|
|
||||||
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.NoContent(http.StatusNoContent)
|
return c.NoContent(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2243,48 +2144,31 @@ func (mh *MediaHandler) CreateMediaBookmark(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()})
|
||||||
}
|
}
|
||||||
|
|
||||||
// The sync-aware path (dedup + LWW + tombstones) is preferred; fall back
|
// The sync-aware path (dedup + LWW + tombstones) is the only path: the
|
||||||
// to the plain query when the service isn't wired (e.g. some tests).
|
// annotation service is a required constructor dependency.
|
||||||
if mh.annotationSvc != nil {
|
origin := req.Origin
|
||||||
origin := req.Origin
|
if origin == "" {
|
||||||
if origin == "" {
|
origin = "web"
|
||||||
origin = "web"
|
|
||||||
}
|
|
||||||
result, err := mh.annotationSvc.SaveBookmark(c.Request().Context(), wsync.SaveBookmarkRequest{
|
|
||||||
MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
|
||||||
UserID: pgtype.UUID{Bytes: userUUID, Valid: true},
|
|
||||||
Title: req.Title,
|
|
||||||
Position: req.Position,
|
|
||||||
Notes: req.Notes,
|
|
||||||
PageNumber: req.PageNumber,
|
|
||||||
ChapterNumber: req.ChapterNumber,
|
|
||||||
CFIPosition: req.CfiPosition,
|
|
||||||
PercentageLoc: req.Percentage,
|
|
||||||
ChapterReference: req.ChapterReference,
|
|
||||||
Source: "web",
|
|
||||||
OriginSource: origin,
|
|
||||||
ModifiedAt: time.Now(),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusCreated, result.Bookmark)
|
|
||||||
}
|
}
|
||||||
|
result, err := mh.annotationSvc.SaveBookmark(c.Request().Context(), wsync.SaveBookmarkRequest{
|
||||||
bookmark, err := mh.db.CreateMediaBookmark(c.Request().Context(), database.CreateMediaBookmarkParams{
|
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},
|
Title: req.Title,
|
||||||
PageNumber: pgtype.Int4{Int32: req.PageNumber, Valid: req.PageNumber > 0},
|
Position: req.Position,
|
||||||
ChapterNumber: pgtype.Int4{Int32: req.ChapterNumber, Valid: req.ChapterNumber > 0},
|
Notes: req.Notes,
|
||||||
CfiPosition: pgtype.Text{String: req.CfiPosition, Valid: req.CfiPosition != ""},
|
PageNumber: req.PageNumber,
|
||||||
Title: req.Title,
|
ChapterNumber: req.ChapterNumber,
|
||||||
Position: pgtype.Text{String: req.Position, Valid: req.Position != ""},
|
CFIPosition: req.CfiPosition,
|
||||||
Notes: pgtype.Text{String: req.Notes, Valid: req.Notes != ""},
|
PercentageLoc: req.Percentage,
|
||||||
|
ChapterReference: req.ChapterReference,
|
||||||
|
Source: "web",
|
||||||
|
OriginSource: origin,
|
||||||
|
ModifiedAt: time.Now(),
|
||||||
})
|
})
|
||||||
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, bookmark)
|
return c.JSON(http.StatusCreated, result.Bookmark)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateMediaBookmark handles PUT /api/media-items/:id/bookmarks/:bookmarkId
|
// UpdateMediaBookmark handles PUT /api/media-items/:id/bookmarks/:bookmarkId
|
||||||
@@ -2340,23 +2224,12 @@ func (mh *MediaHandler) DeleteMediaBookmark(c *echo.Context) error {
|
|||||||
}
|
}
|
||||||
pgUserID := pgtype.UUID{Bytes: userUUID, Valid: true}
|
pgUserID := pgtype.UUID{Bytes: userUUID, Valid: true}
|
||||||
|
|
||||||
if mh.annotationSvc != nil {
|
if err := mh.annotationSvc.TombstoneBookmarkByID(c.Request().Context(), pgUserID, pgBookmarkID, "web"); err != nil {
|
||||||
if err := mh.annotationSvc.TombstoneBookmarkByID(c.Request().Context(), pgUserID, pgBookmarkID, "web"); err != nil {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
return c.JSON(http.StatusNotFound, map[string]string{"error": "bookmark not found"})
|
||||||
return c.JSON(http.StatusNotFound, map[string]string{"error": "bookmark not found"})
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
||||||
}
|
}
|
||||||
return c.NoContent(http.StatusNoContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := mh.db.DeleteMediaBookmark(c.Request().Context(), database.DeleteMediaBookmarkParams{
|
|
||||||
ID: pgBookmarkID,
|
|
||||||
UserID: pgUserID,
|
|
||||||
}); 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.NoContent(http.StatusNoContent)
|
return c.NoContent(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ func registerSyncRoutes(cfg *Config) {
|
|||||||
// Kobo sync routes (device authentication required)
|
// Kobo sync routes (device authentication required)
|
||||||
// Kobo devices use URL path: /api/sync/kobo/{token}/markup
|
// Kobo devices use URL path: /api/sync/kobo/{token}/markup
|
||||||
// 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, cfg.ProgressService)
|
koboHandler := handlers.NewKoboHandler(cfg.Queries, cfg.ConnManager, cfg.ProgressService, cfg.AnnotationService)
|
||||||
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))
|
||||||
|
|||||||
@@ -73,15 +73,15 @@ type SaveHighlightRequest struct {
|
|||||||
// stored CFI drifts range→point shape after device echoes, and the
|
// stored CFI drifts range→point shape after device echoes, and the
|
||||||
// user can edit the selection text — so a key-based upsert would mint
|
// user can edit the selection text — so a key-based upsert would mint
|
||||||
// a duplicate beside the very row being edited.
|
// a duplicate beside the very row being edited.
|
||||||
HighlightID pgtype.UUID
|
HighlightID pgtype.UUID
|
||||||
PercentageStart float64
|
PercentageStart float64
|
||||||
PercentageEnd float64
|
PercentageEnd float64
|
||||||
EpubcfiStart string
|
EpubcfiStart string
|
||||||
EpubcfiEnd string
|
EpubcfiEnd string
|
||||||
ChapterReference int32
|
ChapterReference int32
|
||||||
Source string
|
Source string
|
||||||
ModifiedAt time.Time
|
ModifiedAt time.Time
|
||||||
DeviceSyncData json.RawMessage
|
DeviceSyncData json.RawMessage
|
||||||
// DedupKey overrides the computed key when the client echoes back an
|
// DedupKey overrides the computed key when the client echoes back an
|
||||||
// annotation it received from us (device echoes carry device-native
|
// annotation it received from us (device echoes carry device-native
|
||||||
// locators, so the computed key would never match the original row and
|
// locators, so the computed key would never match the original row and
|
||||||
|
|||||||
+9
-24
@@ -76,27 +76,24 @@ type SyncQueueItem struct {
|
|||||||
// NewSyncQueueProcessor requires the shared ProgressService — queue
|
// NewSyncQueueProcessor requires the shared ProgressService — queue
|
||||||
// progress entries flush through SaveProgress (the LWW-seeded canonical
|
// progress entries flush through SaveProgress (the LWW-seeded canonical
|
||||||
// writer); a nil service has nowhere to fall back to and must not be wired.
|
// writer); a nil service has nowhere to fall back to and must not be wired.
|
||||||
func NewSyncQueueProcessor(db *database.Queries, progressSvc *ProgressService) *SyncQueueProcessor {
|
func NewSyncQueueProcessor(db *database.Queries, progressSvc *ProgressService, annotationSvc *AnnotationService) *SyncQueueProcessor {
|
||||||
return NewSyncQueueProcessorWithConfig(db, progressSvc, 5*time.Second, 50)
|
return NewSyncQueueProcessorWithConfig(db, progressSvc, annotationSvc, 5*time.Second, 50)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSyncQueueProcessorWithConfig constructs a processor with the given flush
|
// NewSyncQueueProcessorWithConfig constructs a processor with the given flush
|
||||||
// interval and batch size. Used at startup to source values from the settings
|
// interval and batch size. Used at startup to source values from the settings
|
||||||
// registry.
|
// registry.
|
||||||
func NewSyncQueueProcessorWithConfig(db *database.Queries, progressSvc *ProgressService, interval time.Duration, batchSize int) *SyncQueueProcessor {
|
func NewSyncQueueProcessorWithConfig(db *database.Queries, progressSvc *ProgressService, annotationSvc *AnnotationService, interval time.Duration, batchSize int) *SyncQueueProcessor {
|
||||||
return &SyncQueueProcessor{
|
return &SyncQueueProcessor{
|
||||||
db: db,
|
db: db,
|
||||||
progressSvc: progressSvc,
|
progressSvc: progressSvc,
|
||||||
progressChan: make(chan *ProgressUpdate, 100),
|
annotationSvc: annotationSvc,
|
||||||
interval: interval,
|
progressChan: make(chan *ProgressUpdate, 100),
|
||||||
batchSize: batchSize,
|
interval: interval,
|
||||||
|
batchSize: batchSize,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SyncQueueProcessor) SetAnnotationService(svc *AnnotationService) {
|
|
||||||
p.annotationSvc = svc
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *SyncQueueProcessor) Start(ctx context.Context) {
|
func (p *SyncQueueProcessor) Start(ctx context.Context) {
|
||||||
log.Printf("Starting sync queue processor (interval: %v, batch: %d)", p.interval, p.batchSize)
|
log.Printf("Starting sync queue processor (interval: %v, batch: %d)", p.interval, p.batchSize)
|
||||||
|
|
||||||
@@ -461,10 +458,6 @@ func (p *SyncQueueProcessor) syncProgress(ctx context.Context, userID pgtype.UUI
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *SyncQueueProcessor) syncNote(ctx context.Context, userID pgtype.UUID, mediaItemID pgtype.UUID, syncData map[string]interface{}) error {
|
func (p *SyncQueueProcessor) syncNote(ctx context.Context, userID pgtype.UUID, mediaItemID pgtype.UUID, syncData map[string]interface{}) error {
|
||||||
if p.annotationSvc == nil {
|
|
||||||
return fmt.Errorf("annotation service not available")
|
|
||||||
}
|
|
||||||
|
|
||||||
req := SaveNoteRequest{
|
req := SaveNoteRequest{
|
||||||
MediaItemID: mediaItemID,
|
MediaItemID: mediaItemID,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
@@ -497,10 +490,6 @@ func (p *SyncQueueProcessor) syncNote(ctx context.Context, userID pgtype.UUID, m
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *SyncQueueProcessor) syncHighlight(ctx context.Context, userID pgtype.UUID, mediaItemID pgtype.UUID, syncData map[string]interface{}) error {
|
func (p *SyncQueueProcessor) syncHighlight(ctx context.Context, userID pgtype.UUID, mediaItemID pgtype.UUID, syncData map[string]interface{}) error {
|
||||||
if p.annotationSvc == nil {
|
|
||||||
return fmt.Errorf("annotation service not available")
|
|
||||||
}
|
|
||||||
|
|
||||||
req := SaveHighlightRequest{
|
req := SaveHighlightRequest{
|
||||||
MediaItemID: mediaItemID,
|
MediaItemID: mediaItemID,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
@@ -548,10 +537,6 @@ func (p *SyncQueueProcessor) syncHighlight(ctx context.Context, userID pgtype.UU
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *SyncQueueProcessor) syncBookmark(ctx context.Context, userID pgtype.UUID, mediaItemID pgtype.UUID, syncData map[string]interface{}) error {
|
func (p *SyncQueueProcessor) syncBookmark(ctx context.Context, userID pgtype.UUID, mediaItemID pgtype.UUID, syncData map[string]interface{}) error {
|
||||||
if p.annotationSvc == nil {
|
|
||||||
return fmt.Errorf("annotation service not available")
|
|
||||||
}
|
|
||||||
|
|
||||||
req := SaveBookmarkRequest{
|
req := SaveBookmarkRequest{
|
||||||
MediaItemID: mediaItemID,
|
MediaItemID: mediaItemID,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCalculateNextRetry(t *testing.T) {
|
func TestCalculateNextRetry(t *testing.T) {
|
||||||
processor := NewSyncQueueProcessor(nil, nil)
|
processor := NewSyncQueueProcessor(nil, nil, nil)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
Reference in new issue
Block a user