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
This commit is contained in:
2026-02-01 12:15:49 -05:00
parent 82ff6356b5
commit 4fe8a81f66
+22 -6
View File
@@ -3,6 +3,8 @@ package handlers
import ( import (
"bookmann/internal/database" "bookmann/internal/database"
"bookmann/internal/opds" "bookmann/internal/opds"
"bookmann/internal/services"
"context"
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
@@ -20,11 +22,17 @@ import (
type OPDSHandler struct { 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{ return &OPDSHandler{
db: db, db: db,
conversionService: conversionService,
} }
} }
@@ -382,11 +390,11 @@ func (h *OPDSHandler) DownloadBook(c echo.Context) error {
mimeType := "" mimeType := ""
if format == "kepub" { if format == "kepub" {
// Check for pre-converted KEPUB
kepubFormat, err := h.db.GetMediaItemFormatByType(c.Request().Context(), database.GetMediaItemFormatByTypeParams{ kepubFormat, err := h.db.GetMediaItemFormatByType(c.Request().Context(), database.GetMediaItemFormatByTypeParams{
MediaItemID: pgtype.UUID{Bytes: bookUUID, Valid: true}, MediaItemID: pgtype.UUID{Bytes: bookUUID, Valid: true},
FormatType: "kepub", FormatType: "kepub",
}) })
if err == nil && kepubFormat.FilePath.Valid { if err == nil && kepubFormat.FilePath.Valid {
filePath = kepubFormat.FilePath.String filePath = kepubFormat.FilePath.String
if kepubFormat.FileSha256.Valid { if kepubFormat.FileSha256.Valid {
@@ -396,12 +404,21 @@ func (h *OPDSHandler) DownloadBook(c echo.Context) error {
mimeType = kepubFormat.MimeType.String mimeType = kepubFormat.MimeType.String
} }
} else { } else {
// Fall back to EPUB (no on-the-fly conversion for now) 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 filePath = mediaItem.FilePath
if mediaItem.MimeType.Valid { if mediaItem.MimeType.Valid {
mimeType = mediaItem.MimeType.String mimeType = mediaItem.MimeType.String
} }
} }
}
} else if format == "pdf" { } else if format == "pdf" {
// Check for PDF format // Check for PDF format
pdfFormat, err := h.db.GetMediaItemFormatByType(c.Request().Context(), database.GetMediaItemFormatByTypeParams{ pdfFormat, err := h.db.GetMediaItemFormatByType(c.Request().Context(), database.GetMediaItemFormatByTypeParams{
@@ -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("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
c.Response().Header().Set("X-Bookmann-UUID", bookUUID.String()) c.Response().Header().Set("X-Bookmann-UUID", bookUUID.String())
if fileSha256 != "" {
c.Response().Header().Set("X-Bookmann-SHA256", fileSha256)
}
if format == "kepub" && fileSha256 != "" { if format == "kepub" && fileSha256 != "" {
c.Response().Header().Set("X-Bookmann-KEPUB-SHA256", fileSha256) c.Response().Header().Set("X-Bookmann-KEPUB-SHA256", fileSha256)
} else if fileSha256 != "" {
c.Response().Header().Set("X-Bookmann-SHA256", fileSha256)
} }
// Stream file // Stream file