rss.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. // Copyright 2017 Frédéric Guillot. All rights reserved.
  2. // Use of this source code is governed by the Apache 2.0
  3. // license that can be found in the LICENSE file.
  4. package rss
  5. import (
  6. "encoding/xml"
  7. "log"
  8. "path"
  9. "strconv"
  10. "strings"
  11. "time"
  12. "github.com/miniflux/miniflux/helper"
  13. "github.com/miniflux/miniflux/model"
  14. "github.com/miniflux/miniflux/reader/date"
  15. )
  16. type rssFeed struct {
  17. XMLName xml.Name `xml:"rss"`
  18. Version string `xml:"version,attr"`
  19. Title string `xml:"channel>title"`
  20. Links []rssLink `xml:"channel>link"`
  21. Language string `xml:"channel>language"`
  22. Description string `xml:"channel>description"`
  23. PubDate string `xml:"channel>pubDate"`
  24. ItunesAuthor string `xml:"http://www.itunes.com/dtds/podcast-1.0.dtd channel>author"`
  25. Items []rssItem `xml:"channel>item"`
  26. }
  27. type rssLink struct {
  28. XMLName xml.Name
  29. Data string `xml:",chardata"`
  30. Href string `xml:"href,attr"`
  31. Rel string `xml:"rel,attr"`
  32. }
  33. type rssItem struct {
  34. GUID string `xml:"guid"`
  35. Title string `xml:"title"`
  36. Links []rssLink `xml:"link"`
  37. OriginalLink string `xml:"http://rssnamespace.org/feedburner/ext/1.0 origLink"`
  38. Description string `xml:"description"`
  39. Content string `xml:"http://purl.org/rss/1.0/modules/content/ encoded"`
  40. PubDate string `xml:"pubDate"`
  41. Date string `xml:"http://purl.org/dc/elements/1.1/ date"`
  42. Authors []rssAuthor `xml:"author"`
  43. Creator string `xml:"http://purl.org/dc/elements/1.1/ creator"`
  44. Enclosures []rssEnclosure `xml:"enclosure"`
  45. OrigEnclosureLink string `xml:"http://rssnamespace.org/feedburner/ext/1.0 origEnclosureLink"`
  46. }
  47. type rssAuthor struct {
  48. XMLName xml.Name
  49. Data string `xml:",chardata"`
  50. Name string `xml:"name"`
  51. }
  52. type rssEnclosure struct {
  53. URL string `xml:"url,attr"`
  54. Type string `xml:"type,attr"`
  55. Length string `xml:"length,attr"`
  56. }
  57. func (r *rssFeed) GetSiteURL() string {
  58. for _, element := range r.Links {
  59. if element.XMLName.Space == "" {
  60. return strings.TrimSpace(element.Data)
  61. }
  62. }
  63. return ""
  64. }
  65. func (r *rssFeed) GetFeedURL() string {
  66. for _, element := range r.Links {
  67. if element.XMLName.Space == "http://www.w3.org/2005/Atom" {
  68. return strings.TrimSpace(element.Href)
  69. }
  70. }
  71. return ""
  72. }
  73. func (r *rssFeed) Transform() *model.Feed {
  74. feed := new(model.Feed)
  75. feed.SiteURL = r.GetSiteURL()
  76. feed.FeedURL = r.GetFeedURL()
  77. feed.Title = strings.TrimSpace(r.Title)
  78. if feed.Title == "" {
  79. feed.Title = feed.SiteURL
  80. }
  81. for _, item := range r.Items {
  82. entry := item.Transform()
  83. if entry.Author == "" && r.ItunesAuthor != "" {
  84. entry.Author = r.ItunesAuthor
  85. }
  86. entry.Author = strings.TrimSpace(entry.Author)
  87. if entry.URL == "" {
  88. entry.URL = feed.SiteURL
  89. }
  90. feed.Entries = append(feed.Entries, entry)
  91. }
  92. return feed
  93. }
  94. func (r *rssItem) GetDate() time.Time {
  95. value := r.PubDate
  96. if r.Date != "" {
  97. value = r.Date
  98. }
  99. if value != "" {
  100. result, err := date.Parse(value)
  101. if err != nil {
  102. log.Println(err)
  103. return time.Now()
  104. }
  105. return result
  106. }
  107. return time.Now()
  108. }
  109. func (r *rssItem) GetAuthor() string {
  110. for _, element := range r.Authors {
  111. if element.Name != "" {
  112. return element.Name
  113. }
  114. if element.Data != "" {
  115. return element.Data
  116. }
  117. }
  118. return r.Creator
  119. }
  120. func (r *rssItem) GetHash() string {
  121. for _, value := range []string{r.GUID, r.GetURL()} {
  122. if value != "" {
  123. return helper.Hash(value)
  124. }
  125. }
  126. return ""
  127. }
  128. func (r *rssItem) GetContent() string {
  129. if r.Content != "" {
  130. return r.Content
  131. }
  132. return r.Description
  133. }
  134. func (r *rssItem) GetURL() string {
  135. if r.OriginalLink != "" {
  136. return r.OriginalLink
  137. }
  138. for _, link := range r.Links {
  139. if link.XMLName.Space == "http://www.w3.org/2005/Atom" && link.Href != "" && isValidLinkRelation(link.Rel) {
  140. return strings.TrimSpace(link.Href)
  141. }
  142. if link.Data != "" {
  143. return strings.TrimSpace(link.Data)
  144. }
  145. }
  146. return ""
  147. }
  148. func (r *rssItem) GetEnclosures() model.EnclosureList {
  149. enclosures := make(model.EnclosureList, 0)
  150. for _, enclosure := range r.Enclosures {
  151. length, _ := strconv.Atoi(enclosure.Length)
  152. enclosureURL := enclosure.URL
  153. if r.OrigEnclosureLink != "" {
  154. filename := path.Base(r.OrigEnclosureLink)
  155. if strings.Contains(enclosureURL, filename) {
  156. enclosureURL = r.OrigEnclosureLink
  157. }
  158. }
  159. enclosures = append(enclosures, &model.Enclosure{
  160. URL: enclosureURL,
  161. MimeType: enclosure.Type,
  162. Size: length,
  163. })
  164. }
  165. return enclosures
  166. }
  167. func (r *rssItem) Transform() *model.Entry {
  168. entry := new(model.Entry)
  169. entry.URL = r.GetURL()
  170. entry.Date = r.GetDate()
  171. entry.Author = r.GetAuthor()
  172. entry.Hash = r.GetHash()
  173. entry.Content = r.GetContent()
  174. entry.Title = strings.TrimSpace(r.Title)
  175. entry.Enclosures = r.GetEnclosures()
  176. if entry.Title == "" {
  177. entry.Title = entry.URL
  178. }
  179. return entry
  180. }
  181. func isValidLinkRelation(rel string) bool {
  182. switch rel {
  183. case "", "alternate", "enclosure", "related", "self", "via":
  184. return true
  185. default:
  186. if strings.HasPrefix(rel, "http") {
  187. return true
  188. }
  189. return false
  190. }
  191. }