diff --git a/cmd/server/main.go b/cmd/server/main.go index 03379ae..ea041a3 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -165,6 +165,7 @@ func main() { DeviceAuthMiddleware: deviceAuthMiddleware, JobsHandler: jobsHandler, LoginTracker: loginAttemptTracker, + LibraryService: libraryService, } // Register all routes and get ebook handler diff --git a/internal/handlers/kobo.go b/internal/handlers/kobo.go index 8e0e10c..3f44442 100644 --- a/internal/handlers/kobo.go +++ b/internal/handlers/kobo.go @@ -4,6 +4,7 @@ import ( "bookhoard/internal/database" wsync "bookhoard/internal/sync" "fmt" + "log" "net/http" "regexp" "strings" @@ -15,9 +16,10 @@ import ( ) type KoboHandler struct { - db *database.Queries - connManager *wsync.ConnectionManager - progressSvc *wsync.ProgressService + db *database.Queries + connManager *wsync.ConnectionManager + progressSvc *wsync.ProgressService + libraryService LibraryPathResolver } func NewKoboHandler(db *database.Queries, connManager *wsync.ConnectionManager) *KoboHandler { @@ -28,6 +30,10 @@ func (h *KoboHandler) SetProgressService(svc *wsync.ProgressService) { h.progressSvc = svc } +func (h *KoboHandler) SetLibraryService(svc LibraryPathResolver) { + h.libraryService = svc +} + // mapContentIdToBookhoardUUID maps Kobo ContentId to Bookhoard UUID with multiple fallback strategies // Enhanced Kobo Sync - ContentId Mapping Logic func (h *KoboHandler) mapContentIdToBookhoardUUID(ctx *echo.Context, contentId string, deviceID uuid.UUID) (uuid.UUID, error, string) { @@ -495,6 +501,23 @@ func (h *KoboHandler) Markup(c *echo.Context) error { chapter := bookmarkSync.Chapter chapterProgress := 0.5 + var convertedCFI *string + var contextText *string + + if epubcfi != "" && h.libraryService != nil { + mediaItem, mErr := h.db.GetMediaItem(c.Request().Context(), pgMediaUUID) + if mErr == nil { + formatGroup := wsync.FormatGroup(mediaItem.FormatGroup) + if formatGroup != wsync.FormatGroupFixedLayout && formatGroup != wsync.FormatGroupComicArchive { + convertedCFI, contextText = h.convertKoboCFIToStandard(c, mediaItem, epubcfi) + } + } + } + + if convertedCFI != nil { + epubcfi = *convertedCFI + } + if h.progressSvc != nil { _, err = h.progressSvc.SaveProgress(c.Request().Context(), wsync.SaveProgressRequest{ MediaItemID: pgMediaUUID, @@ -502,6 +525,7 @@ func (h *KoboHandler) Markup(c *echo.Context) error { Source: "kobo", DeviceID: pgtype.UUID{Bytes: deviceID, Valid: true}, Epubcfi: &epubcfi, + ContextText: contextText, Chapter: &chapter, ChapterProgress: &chapterProgress, DeviceType: "kobo", @@ -778,3 +802,43 @@ func (h *KoboHandler) SyncFromServer(c *echo.Context) error { HighlightsSent: highlightsSent, }) } + +func (h *KoboHandler) convertKoboCFIToStandard(c *echo.Context, mediaItem database.MediaItems, kepubCFI string) (*string, *string) { + epubPath, err := h.libraryService.ResolveMediaPath(c.Request().Context(), mediaItem.LibraryID, mediaItem.FilePath) + if err != nil { + log.Printf("Bookhoard: KEPUB→CFI failed to resolve EPUB path: %v", err) + return nil, nil + } + if epubPath == "" { + log.Printf("Bookhoard: KEPUB→CFI resolved empty EPUB path for %s", mediaItem.FilePath) + return nil, nil + } + + kepubFormat, err := h.db.GetMediaItemFormatByType(c.Request().Context(), database.GetMediaItemFormatByTypeParams{ + MediaItemID: pgtype.UUID{Bytes: mediaItem.ID.Bytes, Valid: true}, + FormatType: "kepub", + }) + if err != nil || !kepubFormat.FilePath.Valid { + return nil, nil + } + + converter := wsync.NewKEPUBCFIConverter(epubPath, kepubFormat.FilePath.String) + result, err := converter.ConvertKEPUBCFIToStandard(kepubCFI, 0.0, "") + if err != nil { + log.Printf("Bookhoard: KEPUB→CFI conversion error: %v", err) + return nil, nil + } + + var cfi *string + if result.CFI != "" { + cfi = &result.CFI + log.Printf("Bookhoard: KEPUB→CFI converted (precision=%s)", result.Precision) + } + + var ctx *string + if result.ExtractedContext != "" { + ctx = &result.ExtractedContext + } + + return cfi, ctx +} diff --git a/internal/router/router.go b/internal/router/router.go index 44a7176..8f25e62 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -66,8 +66,9 @@ type Config struct { LoginTracker *ratelimit.LoginAttemptTracker ScannerHandler *handlers.Handler JobsHandler *handlers.JobsHandler - SidecarHandler *handlers.SidecarHandler + SidecarHandler *handlers.SidecarHandler ReaderHandler *handlers.ReaderHandler + LibraryService *services.LibraryService } // createJWTMiddleware creates a JWT middleware with proper user context setup diff --git a/internal/router/sync.go b/internal/router/sync.go index 9559b96..4731fa6 100644 --- a/internal/router/sync.go +++ b/internal/router/sync.go @@ -33,6 +33,7 @@ func registerSyncRoutes(cfg *Config) { // API clients can use Authorization header: Authorization: Bearer {token} koboHandler := handlers.NewKoboHandler(cfg.Queries, cfg.ConnManager) koboHandler.SetProgressService(cfg.ProgressService) + koboHandler.SetLibraryService(cfg.LibraryService) koboSync := e.Group("/api/sync/kobo/:token") koboSync.POST("/markup", cfg.DeviceAuthMiddleware.Authenticate(koboHandler.Markup)) koboSync.POST("/bookmark", cfg.DeviceAuthMiddleware.Authenticate(koboHandler.Bookmark)) diff --git a/internal/sync/kepub_cfi_converter.go b/internal/sync/kepub_cfi_converter.go index 4a2aad1..cb5d177 100644 --- a/internal/sync/kepub_cfi_converter.go +++ b/internal/sync/kepub_cfi_converter.go @@ -13,9 +13,10 @@ type KEPUBCFIConverter struct { } type KEPUBConversionResult struct { - CFI string - Percentage float64 - Precision string + CFI string + ExtractedContext string + Percentage float64 + Precision string } func NewKEPUBCFIConverter(epubPath, kepubPath string) *KEPUBCFIConverter { @@ -62,9 +63,10 @@ func (k *KEPUBCFIConverter) ConvertKEPUBCFIToStandard(kepubCFI string, percentag cfi, buildErr := buildCFI(spineIndex, matchNode, matchOffset) if buildErr == nil && cfi != "" { return &KEPUBConversionResult{ - CFI: cfi, - Percentage: percentage, - Precision: "exact", + CFI: cfi, + ExtractedContext: searchText, + Percentage: percentage, + Precision: "exact", }, nil } } @@ -72,7 +74,9 @@ func (k *KEPUBCFIConverter) ConvertKEPUBCFIToStandard(kepubCFI string, percentag } } - return k.kepubToStandardByPercentage(spineIndex, percentage) + result, _ := k.kepubToStandardByPercentage(spineIndex, percentage) + result.ExtractedContext = searchText + return result, nil } func (k *KEPUBCFIConverter) ConvertStandardCFIToKEPUB(standardCFI string, percentage float64, contextText string) (*KEPUBConversionResult, error) { @@ -112,9 +116,10 @@ func (k *KEPUBCFIConverter) ConvertStandardCFIToKEPUB(standardCFI string, percen cfi, buildErr := buildCFI(spineIndex, matchNode, matchOffset) if buildErr == nil && cfi != "" { return &KEPUBConversionResult{ - CFI: cfi, - Percentage: percentage, - Precision: "exact", + CFI: cfi, + ExtractedContext: searchText, + Percentage: percentage, + Precision: "exact", }, nil } } @@ -122,7 +127,9 @@ func (k *KEPUBCFIConverter) ConvertStandardCFIToKEPUB(standardCFI string, percen } } - return k.standardToKEPUBByPercentage(spineIndex, percentage) + result, _ := k.standardToKEPUBByPercentage(spineIndex, percentage) + result.ExtractedContext = searchText + return result, nil } func (k *KEPUBCFIConverter) kepubToStandardByPercentage(spineIndex int, percentage float64) (*KEPUBConversionResult, error) {