diff --git a/internal/reader/atom/atom_10.go b/internal/reader/atom/atom_10.go
index 5b67e073..798a8748 100644
--- a/internal/reader/atom/atom_10.go
+++ b/internal/reader/atom/atom_10.go
@@ -91,7 +91,7 @@ type atom10Entry struct {
Content atom10Text `xml:"http://www.w3.org/2005/Atom content"`
Authors atomAuthors `xml:"author"`
Categories []atom10Category `xml:"category"`
- media.Element
+ media.MediaItemElement
}
func (a *atom10Entry) Transform() *model.Entry {
diff --git a/internal/reader/googleplay/googleplay.go b/internal/reader/googleplay/googleplay.go
index 38dcc71f..79404efb 100644
--- a/internal/reader/googleplay/googleplay.go
+++ b/internal/reader/googleplay/googleplay.go
@@ -6,7 +6,7 @@ package googleplay // import "miniflux.app/v2/internal/reader/googleplay"
// Specs:
// https://support.google.com/googleplay/podcasts/answer/6260341
// https://www.google.com/schemas/play-podcasts/1.0/play-podcasts.xsd
-type GooglePlayFeedElement struct {
+type GooglePlayChannelElement struct {
GooglePlayAuthor string `xml:"http://www.google.com/schemas/play-podcasts/1.0 author"`
GooglePlayEmail string `xml:"http://www.google.com/schemas/play-podcasts/1.0 email"`
GooglePlayImage GooglePlayImageElement `xml:"http://www.google.com/schemas/play-podcasts/1.0 image"`
diff --git a/internal/reader/itunes/itunes.go b/internal/reader/itunes/itunes.go
index 1673f306..87a02f0d 100644
--- a/internal/reader/itunes/itunes.go
+++ b/internal/reader/itunes/itunes.go
@@ -6,7 +6,7 @@ package itunes // import "miniflux.app/v2/internal/reader/itunes"
import "strings"
// Specs: https://help.apple.com/itc/podcasts_connect/#/itcb54353390
-type ItunesFeedElement struct {
+type ItunesChannelElement struct {
ItunesAuthor string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd author"`
ItunesBlock string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd block"`
ItunesCategories []ItunesCategoryElement `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd category"`
@@ -22,7 +22,7 @@ type ItunesFeedElement struct {
ItunesType string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd type"`
}
-func (i *ItunesFeedElement) GetItunesCategories() []string {
+func (i *ItunesChannelElement) GetItunesCategories() []string {
var categories []string
for _, category := range i.ItunesCategories {
categories = append(categories, category.Text)
diff --git a/internal/reader/media/media.go b/internal/reader/media/media.go
index df84bf03..7fe4684d 100644
--- a/internal/reader/media/media.go
+++ b/internal/reader/media/media.go
@@ -11,9 +11,8 @@ import (
var textLinkRegex = regexp.MustCompile(`(?mi)(\bhttps?:\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])`)
-// Element represents XML media elements.
// Specs: https://www.rssboard.org/media-rss
-type Element struct {
+type MediaItemElement struct {
MediaGroups []Group `xml:"http://search.yahoo.com/mrss/ group"`
MediaContents []Content `xml:"http://search.yahoo.com/mrss/ content"`
MediaThumbnails []Thumbnail `xml:"http://search.yahoo.com/mrss/ thumbnail"`
@@ -22,7 +21,7 @@ type Element struct {
}
// AllMediaThumbnails returns all thumbnail elements merged together.
-func (e *Element) AllMediaThumbnails() []Thumbnail {
+func (e *MediaItemElement) AllMediaThumbnails() []Thumbnail {
var items []Thumbnail
items = append(items, e.MediaThumbnails...)
for _, mediaGroup := range e.MediaGroups {
@@ -32,7 +31,7 @@ func (e *Element) AllMediaThumbnails() []Thumbnail {
}
// AllMediaContents returns all content elements merged together.
-func (e *Element) AllMediaContents() []Content {
+func (e *MediaItemElement) AllMediaContents() []Content {
var items []Content
items = append(items, e.MediaContents...)
for _, mediaGroup := range e.MediaGroups {
@@ -42,7 +41,7 @@ func (e *Element) AllMediaContents() []Content {
}
// AllMediaPeerLinks returns all peer link elements merged together.
-func (e *Element) AllMediaPeerLinks() []PeerLink {
+func (e *MediaItemElement) AllMediaPeerLinks() []PeerLink {
var items []PeerLink
items = append(items, e.MediaPeerLinks...)
for _, mediaGroup := range e.MediaGroups {
@@ -52,7 +51,7 @@ func (e *Element) AllMediaPeerLinks() []PeerLink {
}
// FirstMediaDescription returns the first description element.
-func (e *Element) FirstMediaDescription() string {
+func (e *MediaItemElement) FirstMediaDescription() string {
description := e.MediaDescriptions.First()
if description != "" {
return description
diff --git a/internal/reader/rdf/adapter.go b/internal/reader/rdf/adapter.go
index 812badbc..bc8c76ed 100644
--- a/internal/reader/rdf/adapter.go
+++ b/internal/reader/rdf/adapter.go
@@ -28,15 +28,14 @@ func (r *RDFAdapter) BuildFeed(feedURL string) *model.Feed {
feed := &model.Feed{
Title: stripTags(r.rdf.Channel.Title),
FeedURL: feedURL,
+ SiteURL: r.rdf.Channel.Link,
}
if feed.Title == "" {
feed.Title = feedURL
}
- if siteURL, err := urllib.AbsoluteURL(feedURL, r.rdf.Channel.Link); err != nil {
- feed.SiteURL = r.rdf.Channel.Link
- } else {
+ if siteURL, err := urllib.AbsoluteURL(feedURL, r.rdf.Channel.Link); err == nil {
feed.SiteURL = siteURL
}
diff --git a/internal/reader/rss/adapter.go b/internal/reader/rss/adapter.go
new file mode 100644
index 00000000..5c1785a9
--- /dev/null
+++ b/internal/reader/rss/adapter.go
@@ -0,0 +1,310 @@
+// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package rss // import "miniflux.app/v2/internal/reader/rss"
+
+import (
+ "html"
+ "log/slog"
+ "path"
+ "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 RSSAdapter struct {
+ rss *RSS
+}
+
+func NewRSSAdapter(rss *RSS) *RSSAdapter {
+ return &RSSAdapter{rss}
+}
+
+func (r *RSSAdapter) BuildFeed(feedURL string) *model.Feed {
+ feed := &model.Feed{
+ Title: html.UnescapeString(strings.TrimSpace(r.rss.Channel.Title)),
+ FeedURL: feedURL,
+ SiteURL: r.rss.Channel.Link,
+ }
+
+ if siteURL, err := urllib.AbsoluteURL(feedURL, r.rss.Channel.Link); err == nil {
+ feed.SiteURL = siteURL
+ }
+
+ // Try to find the feed URL from the Atom links.
+ for _, atomLink := range r.rss.Channel.AtomLinks.Links {
+ atomLinkHref := strings.TrimSpace(atomLink.URL)
+ if atomLinkHref != "" && atomLink.Rel == "self" {
+ if absoluteFeedURL, err := urllib.AbsoluteURL(feedURL, atomLinkHref); err == nil {
+ feed.FeedURL = absoluteFeedURL
+ break
+ }
+ }
+ }
+
+ // Fallback to the site URL if the title is empty.
+ if feed.Title == "" {
+ feed.Title = feed.SiteURL
+ }
+
+ // Get TTL if defined.
+ if r.rss.Channel.TTL != "" {
+ if ttl, err := strconv.Atoi(r.rss.Channel.TTL); err == nil {
+ feed.TTL = ttl
+ }
+ }
+
+ // Get the feed icon URL if defined.
+ if r.rss.Channel.Image != nil {
+ if absoluteIconURL, err := urllib.AbsoluteURL(feed.SiteURL, r.rss.Channel.Image.URL); err == nil {
+ feed.IconURL = absoluteIconURL
+ }
+ }
+
+ for _, item := range r.rss.Channel.Items {
+ entry := model.NewEntry()
+ entry.Author = findEntryAuthor(&item)
+ entry.Date = findEntryDate(&item)
+ entry.Content = findEntryContent(&item)
+ entry.Enclosures = findEntryEnclosures(&item)
+
+ // Populate the entry URL.
+ entryURL := findEntryURL(&item)
+ if entryURL == "" {
+ entry.URL = feed.SiteURL
+ } else {
+ if absoluteEntryURL, err := urllib.AbsoluteURL(feed.SiteURL, entryURL); err == nil {
+ entry.URL = absoluteEntryURL
+ } else {
+ entry.URL = entryURL
+ }
+ }
+
+ // Populate the entry title.
+ entry.Title = findEntryTitle(&item)
+ if entry.Title == "" {
+ entry.Title = sanitizer.TruncateHTML(entry.Content, 100)
+ }
+
+ if entry.Title == "" {
+ entry.Title = entry.URL
+ }
+
+ if entry.Author == "" {
+ entry.Author = findFeedAuthor(&r.rss.Channel)
+ }
+
+ // Generate the entry hash.
+ for _, value := range []string{item.GUID.Data, entryURL} {
+ if value != "" {
+ entry.Hash = crypto.Hash(value)
+ break
+ }
+ }
+
+ // Find CommentsURL if defined.
+ if absoluteCommentsURL := strings.TrimSpace(item.CommentsURL); absoluteCommentsURL != "" && urllib.IsAbsoluteURL(absoluteCommentsURL) {
+ entry.CommentsURL = absoluteCommentsURL
+ }
+
+ // Set podcast listening time.
+ if item.ItunesDuration != "" {
+ if duration, err := getDurationInMinutes(item.ItunesDuration); err == nil {
+ entry.ReadingTime = duration
+ }
+ }
+
+ // Populate entry categories.
+ entry.Tags = append(entry.Tags, item.Categories...)
+ entry.Tags = append(entry.Tags, r.rss.Channel.Categories...)
+ entry.Tags = append(entry.Tags, r.rss.Channel.GetItunesCategories()...)
+
+ if r.rss.Channel.GooglePlayCategory.Text != "" {
+ entry.Tags = append(entry.Tags, r.rss.Channel.GooglePlayCategory.Text)
+ }
+
+ feed.Entries = append(feed.Entries, entry)
+ }
+
+ return feed
+}
+
+func findFeedAuthor(rssChannel *RSSChannel) string {
+ var author string
+ switch {
+ case rssChannel.ItunesAuthor != "":
+ author = rssChannel.ItunesAuthor
+ case rssChannel.GooglePlayAuthor != "":
+ author = rssChannel.GooglePlayAuthor
+ case rssChannel.ItunesOwner.String() != "":
+ author = rssChannel.ItunesOwner.String()
+ case rssChannel.ManagingEditor != "":
+ author = rssChannel.ManagingEditor
+ case rssChannel.Webmaster != "":
+ author = rssChannel.Webmaster
+ }
+ return sanitizer.StripTags(strings.TrimSpace(author))
+}
+
+func findEntryTitle(rssItem *RSSItem) string {
+ title := rssItem.Title
+
+ if rssItem.DublinCoreTitle != "" {
+ title = rssItem.DublinCoreTitle
+ }
+
+ return html.UnescapeString(strings.TrimSpace(title))
+}
+
+func findEntryURL(rssItem *RSSItem) string {
+ for _, link := range []string{rssItem.FeedBurnerLink, rssItem.Link} {
+ if link != "" {
+ return strings.TrimSpace(link)
+ }
+ }
+
+ for _, atomLink := range rssItem.AtomLinks.Links {
+ if atomLink.URL != "" && (strings.EqualFold(atomLink.Rel, "alternate") || atomLink.Rel == "") {
+ return strings.TrimSpace(atomLink.URL)
+ }
+ }
+
+ // Specs: https://cyber.harvard.edu/rss/rss.html#ltguidgtSubelementOfLtitemgt
+ // isPermaLink is optional, its default value is true.
+ // If its value is false, the guid may not be assumed to be a url, or a url to anything in particular.
+ if rssItem.GUID.IsPermaLink == "true" || rssItem.GUID.IsPermaLink == "" {
+ return strings.TrimSpace(rssItem.GUID.Data)
+ }
+
+ return ""
+}
+
+func findEntryContent(rssItem *RSSItem) string {
+ for _, value := range []string{
+ rssItem.DublinCoreContent,
+ rssItem.Description,
+ rssItem.GooglePlayDescription,
+ rssItem.ItunesSummary,
+ rssItem.ItunesSubtitle,
+ } {
+ if value != "" {
+ return value
+ }
+ }
+ return ""
+}
+
+func findEntryDate(rssItem *RSSItem) time.Time {
+ value := rssItem.PubDate
+ if rssItem.DublinCoreDate != "" {
+ value = rssItem.DublinCoreDate
+ }
+
+ if value != "" {
+ result, err := date.Parse(value)
+ if err != nil {
+ slog.Debug("Unable to parse date from RSS feed",
+ slog.String("date", value),
+ slog.String("guid", rssItem.GUID.Data),
+ slog.Any("error", err),
+ )
+ return time.Now()
+ }
+
+ return result
+ }
+
+ return time.Now()
+}
+
+func findEntryAuthor(rssItem *RSSItem) string {
+ var author string
+
+ switch {
+ case rssItem.GooglePlayAuthor != "":
+ author = rssItem.GooglePlayAuthor
+ case rssItem.ItunesAuthor != "":
+ author = rssItem.ItunesAuthor
+ case rssItem.DublinCoreCreator != "":
+ author = rssItem.DublinCoreCreator
+ case rssItem.AtomAuthor.String() != "":
+ author = rssItem.AtomAuthor.String()
+ case strings.Contains(rssItem.Author.Inner, "
+
+
+ My Podcast Feed
+ http://example.org
+ some.email@example.org
+ -
+ Podcasting with RSS
+ http://www.example.org/entries/1
+ An overview of RSS podcasting
+ Fri, 15 Jul 2005 00:00:00 -0500
+ http://www.example.org/entries/1
+
+
+
+
+ `
+
+ feed, err := Parse("https://example.org/", bytes.NewReader([]byte(data)))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if len(feed.Entries) != 1 {
+ t.Errorf("Incorrect number of entries, got: %d", len(feed.Entries))
+ }
+
+ if feed.Entries[0].URL != "http://www.example.org/entries/1" {
+ t.Errorf("Incorrect entry URL, got: %s", feed.Entries[0].URL)
+ }
+
+ if len(feed.Entries[0].Enclosures) != 2 {
+ t.Errorf("Incorrect number of enclosures, got: %d", len(feed.Entries[0].Enclosures))
+ }
+
+ if feed.Entries[0].Enclosures[0].URL != "http://www.example.org/myaudiofile.mp3" {
+ t.Errorf("Incorrect enclosure URL, got: %s", feed.Entries[0].Enclosures[0].URL)
+ }
+
+ if feed.Entries[0].Enclosures[0].MimeType != "audio/mpeg" {
+ t.Errorf("Incorrect enclosure type, got: %s", feed.Entries[0].Enclosures[0].MimeType)
+ }
+
+ if feed.Entries[0].Enclosures[0].Size != 0 {
+ t.Errorf("Incorrect enclosure length, got: %d", feed.Entries[0].Enclosures[0].Size)
+ }
+
+ if feed.Entries[0].Enclosures[1].Size != 0 {
+ t.Errorf("Incorrect enclosure length, got: %d", feed.Entries[0].Enclosures[0].Size)
+ }
+}
+
func TestParseEntryWithEmptyEnclosureURL(t *testing.T) {
data := `
@@ -1306,6 +1359,60 @@ func TestParseEntryWithMediaPeerLink(t *testing.T) {
}
}
+func TestParseItunesDuration(t *testing.T) {
+ data := `
+
+
+ Podcast Example
+ http://www.example.com/index.html
+ -
+ Podcast Episode
+ http://example.com/episode.m4a
+ Tue, 08 Mar 2016 12:00:00 GMT
+ 1:23:45
+
+
+ `
+
+ feed, err := Parse("https://example.org/", bytes.NewReader([]byte(data)))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ expected := 83
+ result := feed.Entries[0].ReadingTime
+ if expected != result {
+ t.Errorf(`Unexpected podcast duration, got %d instead of %d`, result, expected)
+ }
+}
+
+func TestParseIncorrectItunesDuration(t *testing.T) {
+ data := `
+
+
+ Podcast Example
+ http://www.example.com/index.html
+ -
+ Podcast Episode
+ http://example.com/episode.m4a
+ Tue, 08 Mar 2016 12:00:00 GMT
+ invalid
+
+
+ `
+
+ feed, err := Parse("https://example.org/", bytes.NewReader([]byte(data)))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ expected := 0
+ result := feed.Entries[0].ReadingTime
+ if expected != result {
+ t.Errorf(`Unexpected podcast duration, got %d instead of %d`, result, expected)
+ }
+}
+
func TestEntryDescriptionFromItunesSummary(t *testing.T) {
data := `
diff --git a/internal/reader/rss/podcast.go b/internal/reader/rss/podcast.go
index 9a1f365b..7fd93f4a 100644
--- a/internal/reader/rss/podcast.go
+++ b/internal/reader/rss/podcast.go
@@ -12,8 +12,7 @@ import (
var ErrInvalidDurationFormat = errors.New("rss: invalid duration format")
-// normalizeDuration returns the duration tag value as a number of minutes
-func normalizeDuration(rawDuration string) (int, error) {
+func getDurationInMinutes(rawDuration string) (int, error) {
var sumSeconds int
durationParts := strings.Split(rawDuration, ":")
diff --git a/internal/reader/rss/rss.go b/internal/reader/rss/rss.go
index be53c4b0..7935166d 100644
--- a/internal/reader/rss/rss.go
+++ b/internal/reader/rss/rss.go
@@ -5,391 +5,110 @@ package rss // import "miniflux.app/v2/internal/reader/rss"
import (
"encoding/xml"
- "html"
- "log/slog"
- "path"
"strconv"
"strings"
- "time"
- "miniflux.app/v2/internal/crypto"
- "miniflux.app/v2/internal/model"
- "miniflux.app/v2/internal/reader/date"
"miniflux.app/v2/internal/reader/dublincore"
"miniflux.app/v2/internal/reader/googleplay"
"miniflux.app/v2/internal/reader/itunes"
"miniflux.app/v2/internal/reader/media"
- "miniflux.app/v2/internal/reader/sanitizer"
- "miniflux.app/v2/internal/urllib"
)
// Specs: https://www.rssboard.org/rss-specification
-type rssFeed struct {
- XMLName xml.Name `xml:"rss"`
+type RSS struct {
Version string `xml:"rss version,attr"`
- Channel rssChannel `xml:"rss channel"`
+ Channel RSSChannel `xml:"rss channel"`
}
-type rssChannel struct {
- Categories []string `xml:"rss category"`
+type RSSChannel struct {
Title string `xml:"rss title"`
Link string `xml:"rss link"`
- ImageURL string `xml:"rss image>url"`
- Language string `xml:"rss language"`
Description string `xml:"rss description"`
- PubDate string `xml:"rss pubDate"`
+ Language string `xml:"rss language"`
+ Copyright string `xml:"rss copyRight"`
ManagingEditor string `xml:"rss managingEditor"`
Webmaster string `xml:"rss webMaster"`
- TimeToLive rssTTL `xml:"rss ttl"`
- Items []rssItem `xml:"rss item"`
+ PubDate string `xml:"rss pubDate"`
+ LastBuildDate string `xml:"rss lastBuildDate"`
+ Categories []string `xml:"rss category"`
+ Generator string `xml:"rss generator"`
+ Docs string `xml:"rss docs"`
+ Cloud *RSSCloud `xml:"rss cloud"`
+ Image *RSSImage `xml:"rss image"`
+ TTL string `xml:"rss ttl"`
+ SkipHours []string `xml:"rss skipHours>hour"`
+ SkipDays []string `xml:"rss skipDays>day"`
+ Items []RSSItem `xml:"rss item"`
AtomLinks
- itunes.ItunesFeedElement
- googleplay.GooglePlayFeedElement
+ itunes.ItunesChannelElement
+ googleplay.GooglePlayChannelElement
}
-type rssTTL struct {
- Data string `xml:",chardata"`
+type RSSCloud struct {
+ Domain string `xml:"domain,attr"`
+ Port string `xml:"port,attr"`
+ Path string `xml:"path,attr"`
+ RegisterProcedure string `xml:"registerProcedure,attr"`
+ Protocol string `xml:"protocol,attr"`
}
-func (r *rssTTL) Value() int {
- if r.Data == "" {
- return 0
- }
+type RSSImage struct {
+ // URL is the URL of a GIF, JPEG or PNG image that represents the channel.
+ URL string `xml:"url"`
- value, err := strconv.Atoi(r.Data)
- if err != nil {
- return 0
- }
+ // Title describes the image, it's used in the ALT attribute of the HTML tag when the channel is rendered in HTML.
+ Title string `xml:"title"`
- return value
+ // Link is the URL of the site, when the channel is rendered, the image is a link to the site.
+ Link string `xml:"link"`
}
-func (r *rssFeed) Transform(baseURL string) *model.Feed {
- var err error
-
- feed := new(model.Feed)
-
- siteURL := r.siteURL()
- feed.SiteURL, err = urllib.AbsoluteURL(baseURL, siteURL)
- if err != nil {
- feed.SiteURL = siteURL
- }
-
- feedURL := r.feedURL()
- feed.FeedURL, err = urllib.AbsoluteURL(baseURL, feedURL)
- if err != nil {
- feed.FeedURL = feedURL
- }
-
- feed.Title = html.UnescapeString(strings.TrimSpace(r.Channel.Title))
- if feed.Title == "" {
- feed.Title = feed.SiteURL
- }
-
- feed.IconURL = strings.TrimSpace(r.Channel.ImageURL)
- feed.TTL = r.Channel.TimeToLive.Value()
-
- for _, item := range r.Channel.Items {
- entry := item.Transform()
- if entry.Author == "" {
- entry.Author = r.feedAuthor()
- }
-
- if entry.URL == "" {
- entry.URL = feed.SiteURL
- } else {
- entryURL, err := urllib.AbsoluteURL(feed.SiteURL, entry.URL)
- if err == nil {
- entry.URL = entryURL
- }
- }
-
- if entry.Title == "" {
- entry.Title = sanitizer.TruncateHTML(entry.Content, 100)
- }
-
- if entry.Title == "" {
- entry.Title = entry.URL
- }
-
- entry.Tags = append(entry.Tags, r.Channel.Categories...)
- entry.Tags = append(entry.Tags, r.Channel.GetItunesCategories()...)
-
- if r.Channel.GooglePlayCategory.Text != "" {
- entry.Tags = append(entry.Tags, r.Channel.GooglePlayCategory.Text)
- }
-
- feed.Entries = append(feed.Entries, entry)
- }
-
- return feed
-}
-
-func (r *rssFeed) siteURL() string {
- return strings.TrimSpace(r.Channel.Link)
-}
-
-func (r *rssFeed) feedURL() string {
- for _, atomLink := range r.Channel.AtomLinks.Links {
- if atomLink.Rel == "self" {
- return strings.TrimSpace(atomLink.URL)
- }
- }
- return ""
-}
-
-func (r rssFeed) feedAuthor() string {
- var author string
- switch {
- case r.Channel.ItunesAuthor != "":
- author = r.Channel.ItunesAuthor
- case r.Channel.GooglePlayAuthor != "":
- author = r.Channel.GooglePlayAuthor
- case r.Channel.ItunesOwner.String() != "":
- author = r.Channel.ItunesOwner.String()
- case r.Channel.ManagingEditor != "":
- author = r.Channel.ManagingEditor
- case r.Channel.Webmaster != "":
- author = r.Channel.Webmaster
- }
- return sanitizer.StripTags(strings.TrimSpace(author))
-}
-
-type rssGUID struct {
- XMLName xml.Name
- Data string `xml:",chardata"`
- IsPermaLink string `xml:"isPermaLink,attr"`
-}
-
-type rssAuthor struct {
- XMLName xml.Name
- Data string `xml:",chardata"`
- Inner string `xml:",innerxml"`
-}
-
-type rssEnclosure struct {
- URL string `xml:"url,attr"`
- Type string `xml:"type,attr"`
- Length string `xml:"length,attr"`
-}
-
-func (enclosure *rssEnclosure) Size() int64 {
- if enclosure.Length == "" {
- return 0
- }
- size, _ := strconv.ParseInt(enclosure.Length, 10, 0)
- return size
-}
-
-type rssItem struct {
- GUID rssGUID `xml:"rss guid"`
- Title string `xml:"rss title"`
- Link string `xml:"rss link"`
- Description string `xml:"rss description"`
- PubDate string `xml:"rss pubDate"`
- Author rssAuthor `xml:"rss author"`
- Comments string `xml:"rss comments"`
- EnclosureLinks []rssEnclosure `xml:"rss enclosure"`
- Categories []string `xml:"rss category"`
+type RSSItem struct {
+ Title string `xml:"rss title"`
+ Link string `xml:"rss link"`
+ Description string `xml:"rss description"`
+ Author RSSAuthor `xml:"rss author"`
+ Categories []string `xml:"rss category"`
+ CommentsURL string `xml:"rss comments"`
+ Enclosures []RSSEnclosure `xml:"rss enclosure"`
+ GUID RSSGUID `xml:"rss guid"`
+ PubDate string `xml:"rss pubDate"`
+ Source RSSSource `xml:"rss source"`
dublincore.DublinCoreItemElement
- FeedBurnerElement
- media.Element
+ FeedBurnerItemElement
+ media.MediaItemElement
AtomAuthor
AtomLinks
itunes.ItunesItemElement
googleplay.GooglePlayItemElement
}
-func (r *rssItem) Transform() *model.Entry {
- entry := model.NewEntry()
- entry.URL = r.entryURL()
- entry.CommentsURL = r.entryCommentsURL()
- entry.Date = r.entryDate()
- entry.Author = r.entryAuthor()
- entry.Hash = r.entryHash()
- entry.Content = r.entryContent()
- entry.Title = r.entryTitle()
- entry.Enclosures = r.entryEnclosures()
- entry.Tags = r.Categories
- if duration, err := normalizeDuration(r.ItunesDuration); err == nil {
- entry.ReadingTime = duration
- }
-
- return entry
+type RSSAuthor struct {
+ XMLName xml.Name
+ Data string `xml:",chardata"`
+ Inner string `xml:",innerxml"`
}
-func (r *rssItem) entryDate() time.Time {
- value := r.PubDate
- if r.DublinCoreDate != "" {
- value = r.DublinCoreDate
- }
-
- if value != "" {
- result, err := date.Parse(value)
- if err != nil {
- slog.Debug("Unable to parse date from RSS feed",
- slog.String("date", value),
- slog.String("guid", r.GUID.Data),
- slog.Any("error", err),
- )
- return time.Now()
- }
-
- return result
- }
-
- return time.Now()
+type RSSEnclosure struct {
+ URL string `xml:"url,attr"`
+ Type string `xml:"type,attr"`
+ Length string `xml:"length,attr"`
}
-func (r *rssItem) entryAuthor() string {
- var author string
-
- switch {
- case r.GooglePlayAuthor != "":
- author = r.GooglePlayAuthor
- case r.ItunesAuthor != "":
- author = r.ItunesAuthor
- case r.DublinCoreCreator != "":
- author = r.DublinCoreCreator
- case r.AtomAuthor.String() != "":
- author = r.AtomAuthor.String()
- case strings.Contains(r.Author.Inner, "