2022-10-08 12:00:39 +00:00
|
|
|
|
package feeds
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
2023-12-05 10:45:33 +00:00
|
|
|
|
const jsonFeedVersion = "https://jsonfeed.org/version/1.1"
|
2022-10-08 12:00:39 +00:00
|
|
|
|
|
|
|
|
|
// JSONAuthor represents the author of the feed or of an individual item
|
|
|
|
|
// in the feed
|
|
|
|
|
type JSONAuthor struct {
|
|
|
|
|
Name string `json:"name,omitempty"`
|
|
|
|
|
Url string `json:"url,omitempty"`
|
|
|
|
|
Avatar string `json:"avatar,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// JSONAttachment represents a related resource. Podcasts, for instance, would
|
|
|
|
|
// include an attachment that’s an audio or video file.
|
|
|
|
|
type JSONAttachment struct {
|
|
|
|
|
Url string `json:"url,omitempty"`
|
|
|
|
|
MIMEType string `json:"mime_type,omitempty"`
|
|
|
|
|
Title string `json:"title,omitempty"`
|
|
|
|
|
Size int32 `json:"size,omitempty"`
|
|
|
|
|
Duration time.Duration `json:"duration_in_seconds,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MarshalJSON implements the json.Marshaler interface.
|
|
|
|
|
// The Duration field is marshaled in seconds, all other fields are marshaled
|
|
|
|
|
// based upon the definitions in struct tags.
|
|
|
|
|
func (a *JSONAttachment) MarshalJSON() ([]byte, error) {
|
|
|
|
|
type EmbeddedJSONAttachment JSONAttachment
|
|
|
|
|
return json.Marshal(&struct {
|
|
|
|
|
Duration float64 `json:"duration_in_seconds,omitempty"`
|
|
|
|
|
*EmbeddedJSONAttachment
|
|
|
|
|
}{
|
|
|
|
|
EmbeddedJSONAttachment: (*EmbeddedJSONAttachment)(a),
|
|
|
|
|
Duration: a.Duration.Seconds(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UnmarshalJSON implements the json.Unmarshaler interface.
|
|
|
|
|
// The Duration field is expected to be in seconds, all other field types
|
|
|
|
|
// match the struct definition.
|
|
|
|
|
func (a *JSONAttachment) UnmarshalJSON(data []byte) error {
|
|
|
|
|
type EmbeddedJSONAttachment JSONAttachment
|
|
|
|
|
var raw struct {
|
|
|
|
|
Duration float64 `json:"duration_in_seconds,omitempty"`
|
|
|
|
|
*EmbeddedJSONAttachment
|
|
|
|
|
}
|
|
|
|
|
raw.EmbeddedJSONAttachment = (*EmbeddedJSONAttachment)(a)
|
|
|
|
|
|
|
|
|
|
err := json.Unmarshal(data, &raw)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if raw.Duration > 0 {
|
|
|
|
|
nsec := int64(raw.Duration * float64(time.Second))
|
|
|
|
|
raw.EmbeddedJSONAttachment.Duration = time.Duration(nsec)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// JSONItem represents a single entry/post for the feed.
|
|
|
|
|
type JSONItem struct {
|
|
|
|
|
Id string `json:"id"`
|
|
|
|
|
Url string `json:"url,omitempty"`
|
|
|
|
|
ExternalUrl string `json:"external_url,omitempty"`
|
|
|
|
|
Title string `json:"title,omitempty"`
|
|
|
|
|
ContentHTML string `json:"content_html,omitempty"`
|
|
|
|
|
ContentText string `json:"content_text,omitempty"`
|
|
|
|
|
Summary string `json:"summary,omitempty"`
|
|
|
|
|
Image string `json:"image,omitempty"`
|
|
|
|
|
BannerImage string `json:"banner_,omitempty"`
|
|
|
|
|
PublishedDate *time.Time `json:"date_published,omitempty"`
|
|
|
|
|
ModifiedDate *time.Time `json:"date_modified,omitempty"`
|
2023-12-05 10:45:33 +00:00
|
|
|
|
Author *JSONAuthor `json:"author,omitempty"` // deprecated in JSON Feed v1.1, keeping for backwards compatibility
|
|
|
|
|
Authors []*JSONAuthor `json:"authors,omitempty"`
|
2022-10-08 12:00:39 +00:00
|
|
|
|
Tags []string `json:"tags,omitempty"`
|
|
|
|
|
Attachments []JSONAttachment `json:"attachments,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// JSONHub describes an endpoint that can be used to subscribe to real-time
|
|
|
|
|
// notifications from the publisher of this feed.
|
|
|
|
|
type JSONHub struct {
|
|
|
|
|
Type string `json:"type"`
|
|
|
|
|
Url string `json:"url"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// JSONFeed represents a syndication feed in the JSON Feed Version 1 format.
|
|
|
|
|
// Matching the specification found here: https://jsonfeed.org/version/1.
|
|
|
|
|
type JSONFeed struct {
|
2023-12-05 10:45:33 +00:00
|
|
|
|
Version string `json:"version"`
|
|
|
|
|
Title string `json:"title"`
|
|
|
|
|
Language string `json:"language,omitempty"`
|
|
|
|
|
HomePageUrl string `json:"home_page_url,omitempty"`
|
|
|
|
|
FeedUrl string `json:"feed_url,omitempty"`
|
|
|
|
|
Description string `json:"description,omitempty"`
|
|
|
|
|
UserComment string `json:"user_comment,omitempty"`
|
|
|
|
|
NextUrl string `json:"next_url,omitempty"`
|
|
|
|
|
Icon string `json:"icon,omitempty"`
|
|
|
|
|
Favicon string `json:"favicon,omitempty"`
|
|
|
|
|
Author *JSONAuthor `json:"author,omitempty"` // deprecated in JSON Feed v1.1, keeping for backwards compatibility
|
|
|
|
|
Authors []*JSONAuthor `json:"authors,omitempty"`
|
|
|
|
|
Expired *bool `json:"expired,omitempty"`
|
|
|
|
|
Hubs []*JSONHub `json:"hubs,omitempty"`
|
|
|
|
|
Items []*JSONItem `json:"items,omitempty"`
|
2022-10-08 12:00:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// JSON is used to convert a generic Feed to a JSONFeed.
|
|
|
|
|
type JSON struct {
|
|
|
|
|
*Feed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ToJSON encodes f into a JSON string. Returns an error if marshalling fails.
|
|
|
|
|
func (f *JSON) ToJSON() (string, error) {
|
|
|
|
|
return f.JSONFeed().ToJSON()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ToJSON encodes f into a JSON string. Returns an error if marshalling fails.
|
|
|
|
|
func (f *JSONFeed) ToJSON() (string, error) {
|
|
|
|
|
data, err := json.MarshalIndent(f, "", " ")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return string(data), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// JSONFeed creates a new JSONFeed with a generic Feed struct's data.
|
|
|
|
|
func (f *JSON) JSONFeed() *JSONFeed {
|
|
|
|
|
feed := &JSONFeed{
|
|
|
|
|
Version: jsonFeedVersion,
|
|
|
|
|
Title: f.Title,
|
|
|
|
|
Description: f.Description,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if f.Link != nil {
|
|
|
|
|
feed.HomePageUrl = f.Link.Href
|
|
|
|
|
}
|
|
|
|
|
if f.Author != nil {
|
2023-12-05 10:45:33 +00:00
|
|
|
|
author := &JSONAuthor{
|
2022-10-08 12:00:39 +00:00
|
|
|
|
Name: f.Author.Name,
|
|
|
|
|
}
|
2023-12-05 10:45:33 +00:00
|
|
|
|
feed.Author = author
|
|
|
|
|
feed.Authors = []*JSONAuthor{author}
|
2022-10-08 12:00:39 +00:00
|
|
|
|
}
|
|
|
|
|
for _, e := range f.Items {
|
|
|
|
|
feed.Items = append(feed.Items, newJSONItem(e))
|
|
|
|
|
}
|
|
|
|
|
return feed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newJSONItem(i *Item) *JSONItem {
|
|
|
|
|
item := &JSONItem{
|
|
|
|
|
Id: i.Id,
|
|
|
|
|
Title: i.Title,
|
|
|
|
|
Summary: i.Description,
|
|
|
|
|
|
|
|
|
|
ContentHTML: i.Content,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if i.Link != nil {
|
|
|
|
|
item.Url = i.Link.Href
|
|
|
|
|
}
|
|
|
|
|
if i.Source != nil {
|
|
|
|
|
item.ExternalUrl = i.Source.Href
|
|
|
|
|
}
|
|
|
|
|
if i.Author != nil {
|
2023-12-05 10:45:33 +00:00
|
|
|
|
author := &JSONAuthor{
|
2022-10-08 12:00:39 +00:00
|
|
|
|
Name: i.Author.Name,
|
|
|
|
|
}
|
2023-12-05 10:45:33 +00:00
|
|
|
|
item.Author = author
|
|
|
|
|
item.Authors = []*JSONAuthor{author}
|
2022-10-08 12:00:39 +00:00
|
|
|
|
}
|
|
|
|
|
if !i.Created.IsZero() {
|
|
|
|
|
item.PublishedDate = &i.Created
|
|
|
|
|
}
|
|
|
|
|
if !i.Updated.IsZero() {
|
|
|
|
|
item.ModifiedDate = &i.Updated
|
|
|
|
|
}
|
|
|
|
|
if i.Enclosure != nil && strings.HasPrefix(i.Enclosure.Type, "image/") {
|
|
|
|
|
item.Image = i.Enclosure.Url
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return item
|
|
|
|
|
}
|