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:"title"` Author *Author `xml:"author,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 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"` 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 { e := Entry{ ID: id, Title: title, Updated: updated, Links: []Link{}, Metadata: []Meta{}, } if creator != "" { e.Author = &Author{Name: creator} } return e } // 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: "bookhoard", 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 }