From 4fe8a81f66f37d472d4c2cd86ecf5057941c39d2 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sun, 1 Feb 2026 12:15:49 -0500 Subject: [PATCH] feat(opds): add on-the-fly KEPUB conversion for downloads - Integrate conversion service with OPDS handler - Convert EPUB to KEPUB format on download request - Cache converted files to reduce processing time - Support per-device catalog with format availability - Maintain backward compatibility with existing downloads --- internal/handlers/opds.go | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/internal/handlers/opds.go b/internal/handlers/opds.go index 1b95928..3fab35c 100644 --- a/internal/handlers/opds.go +++ b/internal/handlers/opds.go @@ -3,6 +3,8 @@ package handlers import ( "bookmann/internal/database" "bookmann/internal/opds" + "bookmann/internal/services" + "context" "crypto/rand" "encoding/hex" "fmt" @@ -19,12 +21,18 @@ import ( ) type OPDSHandler struct { - db *database.Queries + db *database.Queries + conversionService interface { + ConvertEPUBToKEPUB(ctx context.Context, mediaItemID pgtype.UUID, epubPath string) (*services.ConvertedKEPUB, error) + } } -func NewOPDSHandler(db *database.Queries) *OPDSHandler { +func NewOPDSHandler(db *database.Queries, conversionService interface { + ConvertEPUBToKEPUB(ctx context.Context, mediaItemID pgtype.UUID, epubPath string) (*services.ConvertedKEPUB, error) +}) *OPDSHandler { return &OPDSHandler{ - db: db, + db: db, + conversionService: conversionService, } } @@ -382,11 +390,11 @@ func (h *OPDSHandler) DownloadBook(c echo.Context) error { mimeType := "" if format == "kepub" { - // Check for pre-converted KEPUB kepubFormat, err := h.db.GetMediaItemFormatByType(c.Request().Context(), database.GetMediaItemFormatByTypeParams{ MediaItemID: pgtype.UUID{Bytes: bookUUID, Valid: true}, FormatType: "kepub", }) + if err == nil && kepubFormat.FilePath.Valid { filePath = kepubFormat.FilePath.String if kepubFormat.FileSha256.Valid { @@ -396,10 +404,19 @@ func (h *OPDSHandler) DownloadBook(c echo.Context) error { mimeType = kepubFormat.MimeType.String } } else { - // Fall back to EPUB (no on-the-fly conversion for now) - filePath = mediaItem.FilePath - if mediaItem.MimeType.Valid { - mimeType = mediaItem.MimeType.String + if h.conversionService != nil { + converted, err := h.conversionService.ConvertEPUBToKEPUB(c.Request().Context(), pgtype.UUID{Bytes: bookUUID, Valid: true}, mediaItem.FilePath) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": fmt.Sprintf("KEPUB conversion failed: %v", err)}) + } + filePath = converted.Path + fileSha256 = converted.SHA256 + mimeType = "application/vnd.kobo+xml+zip" + } else { + filePath = mediaItem.FilePath + if mediaItem.MimeType.Valid { + mimeType = mediaItem.MimeType.String + } } } } else if format == "pdf" { @@ -451,11 +468,10 @@ func (h *OPDSHandler) DownloadBook(c echo.Context) error { c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) c.Response().Header().Set("X-Bookmann-UUID", bookUUID.String()) - if fileSha256 != "" { - c.Response().Header().Set("X-Bookmann-SHA256", fileSha256) - } if format == "kepub" && fileSha256 != "" { c.Response().Header().Set("X-Bookmann-KEPUB-SHA256", fileSha256) + } else if fileSha256 != "" { + c.Response().Header().Set("X-Bookmann-SHA256", fileSha256) } // Stream file