From 5d22021e8e32da53f7a9994c7dcc36f18f11de5c Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Tue, 2 Jun 2026 19:45:21 -0400 Subject: [PATCH] fix(opds): epubcfi Atom compatibility, path deduplication, auth tokens - Add and <author><name> elements to Atom feed for compatibility - Remove doubled /opds/opds/ path prefix in feed URLs - Include ?token= auth param on all OPDS URLs - Serve kepub links only for Kobo devices - Fix URL construction for entries and acquisitions --- internal/handlers/opds.go | 51 ++++++++++++++++++++++++++++----------- internal/opds/feed.go | 17 ++++++++++--- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/internal/handlers/opds.go b/internal/handlers/opds.go index 4f5b9f4..9514c12 100644 --- a/internal/handlers/opds.go +++ b/internal/handlers/opds.go @@ -49,6 +49,24 @@ func (h *OPDSHandler) getBaseURLs(c *echo.Context) (string, string, error) { return baseURL.Value, opdsBaseURL, nil } +func (h *OPDSHandler) getAuthToken(c *echo.Context) string { + token := c.QueryParam("token") + if token == "" { + token = strings.TrimPrefix(c.Request().Header.Get("Authorization"), "Bearer ") + } + return token +} + +func appendToken(url, token string) string { + if token == "" { + return url + } + if strings.Contains(url, "?") { + return url + "&token=" + token + } + return url + "?token=" + token +} + // GetDeviceCatalog returns the OPDS catalog feed for a device func (h *OPDSHandler) GetDeviceCatalog(c *echo.Context) error { deviceID := c.Param("deviceId") @@ -140,11 +158,12 @@ func (h *OPDSHandler) GetDeviceCatalog(c *echo.Context) error { ) // Add feed links - catalogURL := fmt.Sprintf("%s/opds/devices/%s/catalog", opdsBaseURL, deviceID) + token := h.getAuthToken(c) + catalogURL := appendToken(fmt.Sprintf("%s/devices/%s/catalog", opdsBaseURL, deviceID), token) feed.AddLink(catalogURL, "application/atom+xml;profile=opds-catalog;kind=acquisition", "self") feed.AddLink(catalogURL, "application/atom+xml;profile=opds-catalog;kind=acquisition", "start") - searchURL := fmt.Sprintf("%s/opds/devices/%s/search", opdsBaseURL, deviceID) + searchURL := appendToken(fmt.Sprintf("%s/devices/%s/search", opdsBaseURL, deviceID), token) feed.AddLink(searchURL, "application/atom+xml;profile=opds-catalog;kind=acquisition", "search") // Add entries @@ -171,14 +190,15 @@ func (h *OPDSHandler) GetDeviceCatalog(c *echo.Context) error { } // Add acquisition links - downloadURL := fmt.Sprintf("%s/opds/devices/%s/download/%s", opdsBaseURL, deviceID, bookUUID) + downloadURL := appendToken(fmt.Sprintf("%s/devices/%s/download/%s", opdsBaseURL, deviceID, bookUUID), token) entry.AddAcquisitionLink(downloadURL, "application/epub+zip") - // Add format variants - kepubURL := fmt.Sprintf("%s?format=kepub", downloadURL) - entry.AddAlternateLink(kepubURL, "application/vnd.kobo+xml+zip") + if device.DeviceType == "kobo" { + kepubURL := downloadURL + "&format=kepub" + entry.AddAlternateLink(kepubURL, "application/vnd.kobo+xml+zip") + } - pdfURL := fmt.Sprintf("%s?format=pdf", downloadURL) + pdfURL := downloadURL + "&format=pdf" entry.AddAlternateLink(pdfURL, "application/pdf") // Add canonical identifier @@ -267,10 +287,11 @@ func (h *OPDSHandler) SearchDeviceCatalog(c *echo.Context) error { ) // Add feed links - catalogURL := fmt.Sprintf("%s/opds/devices/%s/catalog", opdsBaseURL, deviceID) + token := h.getAuthToken(c) + catalogURL := appendToken(fmt.Sprintf("%s/devices/%s/catalog", opdsBaseURL, deviceID), token) feed.AddLink(catalogURL, "application/atom+xml;profile=opds-catalog;kind=acquisition", "start") - searchURL := fmt.Sprintf("%s/opds/devices/%s/search?q=%s", opdsBaseURL, deviceID, query) + searchURL := appendToken(fmt.Sprintf("%s/devices/%s/search?q=%s", opdsBaseURL, deviceID, query), token) feed.AddLink(searchURL, "application/atom+xml;profile=opds-catalog;kind=acquisition", "self") // Add entries (same as catalog) @@ -296,11 +317,13 @@ func (h *OPDSHandler) SearchDeviceCatalog(c *echo.Context) error { entry.SetSummary(item.Description.String) } - downloadURL := fmt.Sprintf("%s/opds/devices/%s/download/%s", opdsBaseURL, deviceID, bookUUID) + downloadURL := appendToken(fmt.Sprintf("%s/devices/%s/download/%s", opdsBaseURL, deviceID, bookUUID), token) entry.AddAcquisitionLink(downloadURL, "application/epub+zip") - kepubURL := fmt.Sprintf("%s?format=kepub", downloadURL) - entry.AddAlternateLink(kepubURL, "application/vnd.kobo+xml+zip") + if device.DeviceType == "kobo" { + kepubURL := downloadURL + "&format=kepub" + entry.AddAlternateLink(kepubURL, "application/vnd.kobo+xml+zip") + } entry.SetIdentifier(bookUUID) @@ -599,7 +622,7 @@ func (h *OPDSHandler) GetDeviceNavigation(c *echo.Context) error { ) // Add feed links - catalogURL := fmt.Sprintf("%s/opds/devices/%s/catalog", opdsBaseURL, deviceID) + catalogURL := fmt.Sprintf("%s/devices/%s/catalog", opdsBaseURL, deviceID) feed.AddLink(catalogURL, "application/atom+xml;profile=opds-catalog;kind=acquisition", "start") feed.AddLink(catalogURL, "application/atom+xml;profile=opds-catalog;kind=acquisition", "self") @@ -791,7 +814,7 @@ func (h *OPDSHandler) RegisterOPDS(c *echo.Context) error { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to create OPDS token"}) } - catalogURL := fmt.Sprintf("%s/opds/devices/%s/catalog", opdsBaseURL, deviceID) + catalogURL := fmt.Sprintf("%s/devices/%s/catalog", opdsBaseURL, deviceID) return c.JSON(http.StatusOK, map[string]interface{}{ "opds_token": map[string]interface{}{ diff --git a/internal/opds/feed.go b/internal/opds/feed.go index 3fbfe46..44cb16d 100644 --- a/internal/opds/feed.go +++ b/internal/opds/feed.go @@ -22,8 +22,8 @@ type Feed struct { type Entry struct { ID string `xml:"id"` - Title string `xml:"dc:title"` - Creator string `xml:"dc:creator,omitempty"` + Title string `xml:"title"` + Author *Author `xml:"author,omitempty"` Updated string `xml:"updated"` Summary string `xml:"summary,omitempty"` Links []Link `xml:"link"` @@ -32,6 +32,12 @@ type Entry struct { Categories []Category `xml:"category,omitempty"` } +type Author struct { + XMLName xml.Name `xml:"author"` + Name string `xml:"name"` + URI string `xml:"uri,omitempty"` +} + type Link struct { Href string `xml:"href,attr"` Type string `xml:"type,attr"` @@ -85,14 +91,17 @@ func (f *Feed) AddEntry(entry Entry) { // NewEntry creates a new OPDS entry func NewEntry(id, title, creator, updated string) Entry { - return Entry{ + e := Entry{ ID: id, Title: title, - Creator: creator, Updated: updated, Links: []Link{}, Metadata: []Meta{}, } + if creator != "" { + e.Author = &Author{Name: creator} + } + return e } // AddAcquisitionLink adds an acquisition link to the entry