aboutsummaryrefslogtreecommitdiff
path: root/internal/reader/atom/atom_10_adapter.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/reader/atom/atom_10_adapter.go')
-rw-r--r--internal/reader/atom/atom_10_adapter.go210
1 files changed, 210 insertions, 0 deletions
diff --git a/internal/reader/atom/atom_10_adapter.go b/internal/reader/atom/atom_10_adapter.go
new file mode 100644
index 00000000..21b90bef
--- /dev/null
+++ b/internal/reader/atom/atom_10_adapter.go
@@ -0,0 +1,210 @@
+// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package atom // import "miniflux.app/v2/internal/reader/atom"
+
+import (
+ "log/slog"
+ "slices"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ "miniflux.app/v2/internal/crypto"
+ "miniflux.app/v2/internal/model"
+ "miniflux.app/v2/internal/reader/date"
+ "miniflux.app/v2/internal/reader/sanitizer"
+ "miniflux.app/v2/internal/urllib"
+)
+
+type Atom10Adapter struct {
+ atomFeed *Atom10Feed
+}
+
+func NewAtom10Adapter(atomFeed *Atom10Feed) *Atom10Adapter {
+ return &Atom10Adapter{atomFeed}
+}
+
+func (a *Atom10Adapter) BuildFeed(baseURL string) *model.Feed {
+ feed := new(model.Feed)
+
+ // Populate the feed URL.
+ feedURL := a.atomFeed.Links.firstLinkWithRelation("self")
+ if feedURL != "" {
+ if absoluteFeedURL, err := urllib.AbsoluteURL(baseURL, feedURL); err == nil {
+ feed.FeedURL = absoluteFeedURL
+ }
+ } else {
+ feed.FeedURL = baseURL
+ }
+
+ // Populate the site URL.
+ siteURL := a.atomFeed.Links.OriginalLink()
+ if siteURL != "" {
+ if absoluteSiteURL, err := urllib.AbsoluteURL(baseURL, siteURL); err == nil {
+ feed.SiteURL = absoluteSiteURL
+ }
+ } else {
+ feed.SiteURL = baseURL
+ }
+
+ // Populate the feed title.
+ feed.Title = a.atomFeed.Title.Body()
+ if feed.Title == "" {
+ feed.Title = feed.SiteURL
+ }
+
+ // Populate the feed icon.
+ if a.atomFeed.Icon != "" {
+ if absoluteIconURL, err := urllib.AbsoluteURL(feed.SiteURL, a.atomFeed.Icon); err == nil {
+ feed.IconURL = absoluteIconURL
+ }
+ } else if a.atomFeed.Logo != "" {
+ if absoluteLogoURL, err := urllib.AbsoluteURL(feed.SiteURL, a.atomFeed.Logo); err == nil {
+ feed.IconURL = absoluteLogoURL
+ }
+ }
+
+ for _, atomEntry := range a.atomFeed.Entries {
+ entry := model.NewEntry()
+
+ // Populate the entry URL.
+ entry.URL = atomEntry.Links.OriginalLink()
+ if entry.URL != "" {
+ if absoluteEntryURL, err := urllib.AbsoluteURL(feed.SiteURL, entry.URL); err == nil {
+ entry.URL = absoluteEntryURL
+ }
+ }
+
+ // Populate the entry content.
+ entry.Content = atomEntry.Content.Body()
+ if entry.Content == "" {
+ entry.Content = atomEntry.Summary.Body()
+ }
+ if entry.Content == "" {
+ entry.Content = atomEntry.FirstMediaDescription()
+ }
+
+ // Populate the entry title.
+ entry.Title = atomEntry.Title.Title()
+ if entry.Title == "" {
+ entry.Title = sanitizer.TruncateHTML(entry.Content, 100)
+ }
+ if entry.Title == "" {
+ entry.Title = entry.URL
+ }
+
+ // Populate the entry author.
+ authors := atomEntry.Authors.PersonNames()
+ if len(authors) == 0 {
+ authors = append(authors, a.atomFeed.Authors.PersonNames()...)
+ }
+ authors = slices.Compact(authors)
+ sort.Strings(authors)
+ entry.Author = strings.Join(authors, ", ")
+
+ // Populate the entry date.
+ for _, value := range []string{atomEntry.Published, atomEntry.Updated} {
+ if parsedDate, err := date.Parse(value); err != nil {
+ slog.Debug("Unable to parse date from Atom 1.0 feed",
+ slog.String("date", value),
+ slog.String("url", entry.URL),
+ slog.Any("error", err),
+ )
+ } else {
+ entry.Date = parsedDate
+ break
+ }
+ }
+ if entry.Date.IsZero() {
+ entry.Date = time.Now()
+ }
+
+ // Populate categories.
+ categories := atomEntry.Categories.CategoryNames()
+ if len(categories) == 0 {
+ categories = append(categories, a.atomFeed.Categories.CategoryNames()...)
+ }
+ if len(categories) > 0 {
+ categories = slices.Compact(categories)
+ sort.Strings(categories)
+ entry.Tags = categories
+ }
+
+ // Populate the commentsURL if defined.
+ // See https://tools.ietf.org/html/rfc4685#section-4
+ // If the type attribute of the atom:link is omitted, its value is assumed to be "application/atom+xml".
+ // We accept only HTML or XHTML documents for now since the intention is to have the same behavior as RSS.
+ commentsURL := atomEntry.Links.firstLinkWithRelationAndType("replies", "text/html", "application/xhtml+xml")
+ if urllib.IsAbsoluteURL(commentsURL) {
+ entry.CommentsURL = commentsURL
+ }
+
+ // Generate the entry hash.
+ for _, value := range []string{atomEntry.ID, atomEntry.Links.OriginalLink()} {
+ if value != "" {
+ entry.Hash = crypto.Hash(value)
+ break
+ }
+ }
+
+ // Populate the entry enclosures.
+ uniqueEnclosuresMap := make(map[string]bool)
+
+ for _, mediaThumbnail := range atomEntry.AllMediaThumbnails() {
+ if _, found := uniqueEnclosuresMap[mediaThumbnail.URL]; !found {
+ uniqueEnclosuresMap[mediaThumbnail.URL] = true
+ entry.Enclosures = append(entry.Enclosures, &model.Enclosure{
+ URL: mediaThumbnail.URL,
+ MimeType: mediaThumbnail.MimeType(),
+ Size: mediaThumbnail.Size(),
+ })
+ }
+ }
+
+ for _, link := range atomEntry.Links {
+ if strings.EqualFold(link.Rel, "enclosure") {
+ if link.Href == "" {
+ continue
+ }
+
+ if _, found := uniqueEnclosuresMap[link.Href]; !found {
+ uniqueEnclosuresMap[link.Href] = true
+ length, _ := strconv.ParseInt(link.Length, 10, 0)
+ entry.Enclosures = append(entry.Enclosures, &model.Enclosure{
+ URL: link.Href,
+ MimeType: link.Type,
+ Size: length,
+ })
+ }
+ }
+ }
+
+ for _, mediaContent := range atomEntry.AllMediaContents() {
+ if _, found := uniqueEnclosuresMap[mediaContent.URL]; !found {
+ uniqueEnclosuresMap[mediaContent.URL] = true
+ entry.Enclosures = append(entry.Enclosures, &model.Enclosure{
+ URL: mediaContent.URL,
+ MimeType: mediaContent.MimeType(),
+ Size: mediaContent.Size(),
+ })
+ }
+ }
+
+ for _, mediaPeerLink := range atomEntry.AllMediaPeerLinks() {
+ if _, found := uniqueEnclosuresMap[mediaPeerLink.URL]; !found {
+ uniqueEnclosuresMap[mediaPeerLink.URL] = true
+ entry.Enclosures = append(entry.Enclosures, &model.Enclosure{
+ URL: mediaPeerLink.URL,
+ MimeType: mediaPeerLink.MimeType(),
+ Size: mediaPeerLink.Size(),
+ })
+ }
+ }
+
+ feed.Entries = append(feed.Entries, entry)
+ }
+
+ return feed
+}