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