diff --git a/internal/handlers/opds.go b/internal/handlers/opds.go index 9514c12..a847274 100644 --- a/internal/handlers/opds.go +++ b/internal/handlers/opds.go @@ -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 {