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  }