feat(opds): serve comic archives in native format with correct mime types

The OPDS device catalog previously hard-coded every acquisition link as
application/epub+zip and always offered kepub/pdf alternate links, which
is wrong for comic archives (cbz/cbr/cb7/cbt) and other non-epub media.

- Resolve the acquisition mime type from the media item's stored
  mime_type (falling back to format_mimetype, then epub) instead of
  assuming epub
- Only offer reflowable conversions (kepub for kobo, pdf) for ebooks;
  comic archives are served as-is in their native format
- Derive the native format label (epub/pdf/cbz/cbr/...) from the file
  path in ListFormats rather than always reporting epub
- Add resolveMimeType, isComicArchive, and formatLabelFromPath helpers
This commit is contained in:
2026-06-07 00:03:18 -04:00
parent 3170aa6b77
commit b1d2ccc87c
+67 -12
View File
@@ -67,6 +67,55 @@ func appendToken(url, token string) string {
return url + "?token=" + token
}
// resolveMimeType returns the mime type for a media item, preferring the stored
// mime_type, then format_mimetype, and finally falling back to EPUB.
func resolveMimeType(mime, formatMime pgtype.Text) string {
if mime.Valid && mime.String != "" {
return mime.String
}
if formatMime.Valid && formatMime.String != "" {
return formatMime.String
}
return "application/epub+zip"
}
// isComicArchive reports whether a format group represents a comic/manga
// archive (cbz/cbr/cb7/cbt). Comic archives are served in their native format
// and should not be offered as EPUB/KEPUB/PDF conversions.
func isComicArchive(formatGroup string) bool {
return strings.EqualFold(formatGroup, "comic_archive")
}
// formatLabelFromPath derives a short format label (e.g. "epub", "cbz") from a
// file path's extension, defaulting to "epub" when it cannot be determined.
func formatLabelFromPath(path string) string {
ext := strings.ToLower(filepath.Ext(path))
switch ext {
case ".epub":
return "epub"
case ".pdf":
return "pdf"
case ".cbz":
return "cbz"
case ".cbr":
return "cbr"
case ".cb7":
return "cb7"
case ".cbt":
return "cbt"
case ".mobi":
return "mobi"
case ".azw", ".azw3":
return "azw3"
case ".txt":
return "txt"
case "":
return "epub"
default:
return strings.TrimPrefix(ext, ".")
}
}
// GetDeviceCatalog returns the OPDS catalog feed for a device
func (h *OPDSHandler) GetDeviceCatalog(c *echo.Context) error {
deviceID := c.Param("deviceId")
@@ -189,18 +238,22 @@ func (h *OPDSHandler) GetDeviceCatalog(c *echo.Context) error {
entry.SetSummary(item.Description.String)
}
// Add acquisition links
// Add acquisition link using the item's real mime type
downloadURL := appendToken(fmt.Sprintf("%s/devices/%s/download/%s", opdsBaseURL, deviceID, bookUUID), token)
entry.AddAcquisitionLink(downloadURL, "application/epub+zip")
entry.AddAcquisitionLink(downloadURL, resolveMimeType(item.MimeType, item.FormatMimetype))
if device.DeviceType == "kobo" {
kepubURL := downloadURL + "&format=kepub"
entry.AddAlternateLink(kepubURL, "application/vnd.kobo+xml+zip")
// Only offer reflowable conversions (kepub/pdf) for ebooks; comic
// archives are served as-is in their native format.
if !isComicArchive(item.FormatGroup) {
if device.DeviceType == "kobo" {
kepubURL := downloadURL + "&format=kepub"
entry.AddAlternateLink(kepubURL, "application/vnd.kobo+xml+zip")
}
pdfURL := downloadURL + "&format=pdf"
entry.AddAlternateLink(pdfURL, "application/pdf")
}
pdfURL := downloadURL + "&format=pdf"
entry.AddAlternateLink(pdfURL, "application/pdf")
// Add canonical identifier
entry.SetIdentifier(bookUUID)
@@ -318,9 +371,11 @@ func (h *OPDSHandler) SearchDeviceCatalog(c *echo.Context) error {
}
downloadURL := appendToken(fmt.Sprintf("%s/devices/%s/download/%s", opdsBaseURL, deviceID, bookUUID), token)
entry.AddAcquisitionLink(downloadURL, "application/epub+zip")
entry.AddAcquisitionLink(downloadURL, resolveMimeType(item.MimeType, item.FormatMimetype))
if device.DeviceType == "kobo" {
// Only offer kepub conversion for ebooks; comic archives are served
// as-is in their native format.
if !isComicArchive(item.FormatGroup) && device.DeviceType == "kobo" {
kepubURL := downloadURL + "&format=kepub"
entry.AddAlternateLink(kepubURL, "application/vnd.kobo+xml+zip")
}
@@ -705,14 +760,14 @@ func (h *OPDSHandler) ListFormats(c *echo.Context) error {
formatList := []FormatInfo{}
// Add EPUB format (always available if media item exists)
// Add the primary/native format (always available if media item exists)
fileSize := int64(0)
if mediaItem.FileSize.Valid {
fileSize = mediaItem.FileSize.Int64
}
formatList = append(formatList, FormatInfo{
FormatType: "epub",
FormatType: formatLabelFromPath(mediaItem.FilePath),
FilePath: mediaItem.FilePath,
FileSha256: func() string {
if mediaItem.FileSha256.Valid {