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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user