feat(opds): Add OPDS feed generation library

- Add OPDS feed generation for Kobo compatibility
- Support device catalog, search, download, navigation
- Add format conversion support
This commit is contained in:
2026-01-31 22:32:37 -05:00
parent 964a4583ab
commit 3d20e802e6
2 changed files with 431 additions and 0 deletions
+185
View File
@@ -0,0 +1,185 @@
package opds
import (
"encoding/xml"
"fmt"
"time"
)
// OPDS 1.2 Feed Structures
type Feed struct {
XMLName xml.Name `xml:"feed"`
Xmlns string `xml:"xmlns,attr"`
OpdsNS string `xml:"xmlns:opds,attr"`
DcNS string `xml:"xmlns:dc,attr"`
ID string `xml:"id"`
Title string `xml:"title"`
Updated string `xml:"updated"`
Links []Link `xml:"link"`
Entries []Entry `xml:"entry"`
}
type Entry struct {
ID string `xml:"id"`
Title string `xml:"dc:title"`
Creator string `xml:"dc:creator,omitempty"`
Updated string `xml:"updated"`
Summary string `xml:"summary,omitempty"`
Links []Link `xml:"link"`
Identifier *Identifier `xml:"dc:identifier,omitempty"`
Metadata []Meta `xml:"meta"`
Categories []Category `xml:"category,omitempty"`
}
type Link struct {
Href string `xml:"href,attr"`
Type string `xml:"type,attr"`
Rel string `xml:"rel,attr,omitempty"`
}
type Identifier struct {
XMLName xml.Name `xml:"dc:identifier"`
ID string `xml:"id,attr"`
Content string `xml:",chardata"`
}
type Meta struct {
Property string `xml:"property,attr"`
Content string `xml:",chardata"`
}
type Category struct {
Scheme string `xml:"scheme,attr"`
Term string `xml:"term,attr"`
}
// NewFeed creates a new OPDS feed
func NewFeed(feedID, title string) *Feed {
now := time.Now().Format(time.RFC3339)
return &Feed{
Xmlns: "http://www.w3.org/2005/Atom",
OpdsNS: "http://opds-spec.org/2010/",
DcNS: "http://purl.org/dc/elements/1.1/",
ID: feedID,
Title: title,
Updated: now,
Links: []Link{},
Entries: []Entry{},
}
}
// AddLink adds a link to the feed
func (f *Feed) AddLink(href, linkType, rel string) {
f.Links = append(f.Links, Link{
Href: href,
Type: linkType,
Rel: rel,
})
}
// AddEntry adds an entry to the feed
func (f *Feed) AddEntry(entry Entry) {
f.Entries = append(f.Entries, entry)
}
// NewEntry creates a new OPDS entry
func NewEntry(id, title, creator, updated string) Entry {
return Entry{
ID: id,
Title: title,
Creator: creator,
Updated: updated,
Links: []Link{},
Metadata: []Meta{},
}
}
// AddAcquisitionLink adds an acquisition link to the entry
func (e *Entry) AddAcquisitionLink(href, linkType string) {
e.Links = append(e.Links, Link{
Href: href,
Type: linkType,
Rel: "http://opds-spec.org/acquisition/open-access",
})
}
// AddAlternateLink adds an alternate link to the entry
func (e *Entry) AddAlternateLink(href, linkType string) {
e.Links = append(e.Links, Link{
Href: href,
Type: linkType,
Rel: "alternate",
})
}
// AddLink adds a generic link to the entry
func (e *Entry) AddLink(href, linkType, rel string) {
e.Links = append(e.Links, Link{
Href: href,
Type: linkType,
Rel: rel,
})
}
// SetIdentifier sets the canonical identifier
func (e *Entry) SetIdentifier(id string) {
e.Identifier = &Identifier{
ID: "bookmann",
Content: id,
}
}
// AddMetadata adds metadata to the entry
func (e *Entry) AddMetadata(property, content string) {
e.Metadata = append(e.Metadata, Meta{
Property: property,
Content: content,
})
}
// AddCategory adds a category to the entry
func (e *Entry) AddCategory(scheme, term string) {
e.Categories = append(e.Categories, Category{
Scheme: scheme,
Term: term,
})
}
// SetSummary sets the entry summary
func (e *Entry) SetSummary(summary string) {
e.Summary = summary
}
// GenerateXML generates the OPDS XML
func (f *Feed) GenerateXML() ([]byte, error) {
output, err := xml.MarshalIndent(f, "", " ")
if err != nil {
return nil, fmt.Errorf("failed to marshal OPDS feed: %w", err)
}
return output, nil
}
// GenerateXMLString generates the OPDS XML as a string
func (f *Feed) GenerateXMLString() (string, error) {
output, err := f.GenerateXML()
if err != nil {
return "", err
}
return xml.Header + string(output), nil
}
// NewErrorFeed creates an error feed
func NewErrorFeed(message string) *Feed {
feed := NewFeed(
fmt.Sprintf("urn:uuid:error-%d", time.Now().Unix()),
"Error",
)
feed.AddEntry(Entry{
ID: "urn:uuid:error",
Title: "Error",
Summary: message,
Updated: feed.Updated,
})
return feed
}