json.go (5434B)
1 package feeds 2 3 import ( 4 "encoding/json" 5 "strings" 6 "time" 7 ) 8 9 const jsonFeedVersion = "https://jsonfeed.org/version/1" 10 11 // JSONAuthor represents the author of the feed or of an individual item 12 // in the feed 13 type JSONAuthor struct { 14 Name string `json:"name,omitempty"` 15 Url string `json:"url,omitempty"` 16 Avatar string `json:"avatar,omitempty"` 17 } 18 19 // JSONAttachment represents a related resource. Podcasts, for instance, would 20 // include an attachment that’s an audio or video file. 21 type JSONAttachment struct { 22 Url string `json:"url,omitempty"` 23 MIMEType string `json:"mime_type,omitempty"` 24 Title string `json:"title,omitempty"` 25 Size int32 `json:"size,omitempty"` 26 Duration time.Duration `json:"duration_in_seconds,omitempty"` 27 } 28 29 // MarshalJSON implements the json.Marshaler interface. 30 // The Duration field is marshaled in seconds, all other fields are marshaled 31 // based upon the definitions in struct tags. 32 func (a *JSONAttachment) MarshalJSON() ([]byte, error) { 33 type EmbeddedJSONAttachment JSONAttachment 34 return json.Marshal(&struct { 35 Duration float64 `json:"duration_in_seconds,omitempty"` 36 *EmbeddedJSONAttachment 37 }{ 38 EmbeddedJSONAttachment: (*EmbeddedJSONAttachment)(a), 39 Duration: a.Duration.Seconds(), 40 }) 41 } 42 43 // UnmarshalJSON implements the json.Unmarshaler interface. 44 // The Duration field is expected to be in seconds, all other field types 45 // match the struct definition. 46 func (a *JSONAttachment) UnmarshalJSON(data []byte) error { 47 type EmbeddedJSONAttachment JSONAttachment 48 var raw struct { 49 Duration float64 `json:"duration_in_seconds,omitempty"` 50 *EmbeddedJSONAttachment 51 } 52 raw.EmbeddedJSONAttachment = (*EmbeddedJSONAttachment)(a) 53 54 err := json.Unmarshal(data, &raw) 55 if err != nil { 56 return err 57 } 58 59 if raw.Duration > 0 { 60 nsec := int64(raw.Duration * float64(time.Second)) 61 raw.EmbeddedJSONAttachment.Duration = time.Duration(nsec) 62 } 63 64 return nil 65 } 66 67 // JSONItem represents a single entry/post for the feed. 68 type JSONItem struct { 69 Id string `json:"id"` 70 Url string `json:"url,omitempty"` 71 ExternalUrl string `json:"external_url,omitempty"` 72 Title string `json:"title,omitempty"` 73 ContentHTML string `json:"content_html,omitempty"` 74 ContentText string `json:"content_text,omitempty"` 75 Summary string `json:"summary,omitempty"` 76 Image string `json:"image,omitempty"` 77 BannerImage string `json:"banner_,omitempty"` 78 PublishedDate *time.Time `json:"date_published,omitempty"` 79 ModifiedDate *time.Time `json:"date_modified,omitempty"` 80 Author *JSONAuthor `json:"author,omitempty"` 81 Tags []string `json:"tags,omitempty"` 82 Attachments []JSONAttachment `json:"attachments,omitempty"` 83 } 84 85 // JSONHub describes an endpoint that can be used to subscribe to real-time 86 // notifications from the publisher of this feed. 87 type JSONHub struct { 88 Type string `json:"type"` 89 Url string `json:"url"` 90 } 91 92 // JSONFeed represents a syndication feed in the JSON Feed Version 1 format. 93 // Matching the specification found here: https://jsonfeed.org/version/1. 94 type JSONFeed struct { 95 Version string `json:"version"` 96 Title string `json:"title"` 97 HomePageUrl string `json:"home_page_url,omitempty"` 98 FeedUrl string `json:"feed_url,omitempty"` 99 Description string `json:"description,omitempty"` 100 UserComment string `json:"user_comment,omitempty"` 101 NextUrl string `json:"next_url,omitempty"` 102 Icon string `json:"icon,omitempty"` 103 Favicon string `json:"favicon,omitempty"` 104 Author *JSONAuthor `json:"author,omitempty"` 105 Expired *bool `json:"expired,omitempty"` 106 Hubs []*JSONItem `json:"hubs,omitempty"` 107 Items []*JSONItem `json:"items,omitempty"` 108 } 109 110 // JSON is used to convert a generic Feed to a JSONFeed. 111 type JSON struct { 112 *Feed 113 } 114 115 // ToJSON encodes f into a JSON string. Returns an error if marshalling fails. 116 func (f *JSON) ToJSON() (string, error) { 117 return f.JSONFeed().ToJSON() 118 } 119 120 // ToJSON encodes f into a JSON string. Returns an error if marshalling fails. 121 func (f *JSONFeed) ToJSON() (string, error) { 122 data, err := json.MarshalIndent(f, "", " ") 123 if err != nil { 124 return "", err 125 } 126 127 return string(data), nil 128 } 129 130 // JSONFeed creates a new JSONFeed with a generic Feed struct's data. 131 func (f *JSON) JSONFeed() *JSONFeed { 132 feed := &JSONFeed{ 133 Version: jsonFeedVersion, 134 Title: f.Title, 135 Description: f.Description, 136 } 137 138 if f.Link != nil { 139 feed.HomePageUrl = f.Link.Href 140 } 141 if f.Author != nil { 142 feed.Author = &JSONAuthor{ 143 Name: f.Author.Name, 144 } 145 } 146 for _, e := range f.Items { 147 feed.Items = append(feed.Items, newJSONItem(e)) 148 } 149 return feed 150 } 151 152 func newJSONItem(i *Item) *JSONItem { 153 item := &JSONItem{ 154 Id: i.Id, 155 Title: i.Title, 156 Summary: i.Description, 157 158 ContentHTML: i.Content, 159 } 160 161 if i.Link != nil { 162 item.Url = i.Link.Href 163 } 164 if i.Source != nil { 165 item.ExternalUrl = i.Source.Href 166 } 167 if i.Author != nil { 168 item.Author = &JSONAuthor{ 169 Name: i.Author.Name, 170 } 171 } 172 if !i.Created.IsZero() { 173 item.PublishedDate = &i.Created 174 } 175 if !i.Updated.IsZero() { 176 item.ModifiedDate = &i.Updated 177 } 178 if i.Enclosure != nil && strings.HasPrefix(i.Enclosure.Type, "image/") { 179 item.Image = i.Enclosure.Url 180 } 181 182 return item 183 }