Repository: mmcdole/gofeed
Pull request #190 (2022-05-27 16:58:09)

ISSUE 189 - Multiple enclosures in rss
Multiple enclosures in RSS are not handled.
Rather we get the last one.

1. Added Multiple enclosure handling to RSS and translator.
2. Updated test data.
3. Added specific test coverage for multiple enclosures.

Note: since the RSS parser seemed to accumulate the last enclosure,
and select that one. I reversed the accumulation array, so that,
anyone expecting the old behavior would not be surprised.

mmcdole: @rodmcelrath looking at this, I think this would still be a breaking change, right?  If anyone has code that is accessing the `enclosure` property, this would result in a compilation error now.

I'd be supportive of handling multiple enclosures, but I'd want to do it in a 100% backward compatible way.  That would probably mean maintaining the old single `enclosure` property and having a new `enclosures` property, where `enclosures` would include all enclosures in an item and `enclosure` would just be the last one, or whatever the old behavior was.

Then, rather than modifying any existing tests, I'd just expect new tests to cover the new property.
rodmcelrath: Alright - how about this.

I put the 'Enclosure' field back.  There is separate test coverage.  I did, however have to modify the two other tests to include the new 'Enclosures' field - as the parser is as the parser does.
mmcdole: Looks great! Thank you for your contribution!

Patch:
diff --git a/.gitignore b/.gitignore
index 1340f5b..c6eaf44 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,10 @@
 *.a
 *.so
 
+#vim stuff
+*.swp
+*.swo
+
 # Folders
 _obj
 _test
diff --git a/rss/feed.go b/rss/feed.go
index dd9c9e5..c32ed7f 100644
--- a/rss/feed.go
+++ b/rss/feed.go
@@ -52,6 +52,7 @@ type Item struct {
 	Categories    []*Category              `json:"categories,omitempty"`
 	Comments      string                   `json:"comments,omitempty"`
 	Enclosure     *Enclosure               `json:"enclosure,omitempty"`
+	Enclosures    []*Enclosure             `json:"enclosures,omitempty"`
 	GUID          *GUID                    `json:"guid,omitempty"`
 	PubDate       string                   `json:"pubDate,omitempty"`
 	PubDateParsed *time.Time               `json:"pubDateParsed,omitempty"`
diff --git a/rss/parser.go b/rss/parser.go
index 1bbd46c..ce664cd 100644
--- a/rss/parser.go
+++ b/rss/parser.go
@@ -319,6 +319,7 @@ func (rp *Parser) parseItem(p *xpp.XMLPullParser) (item *Item, err error) {
 	item = &Item{}
 	extensions := ext.Extensions{}
 	categories := []*Category{}
+	enclosures := []*Enclosure{}
 
 	for {
 		tok, err := rp.base.NextTag(p)
@@ -402,6 +403,7 @@ func (rp *Parser) parseItem(p *xpp.XMLPullParser) (item *Item, err error) {
 					return nil, err
 				}
 				item.Enclosure = result
+				enclosures = append(enclosures, result)
 			} else if name == "guid" {
 				result, err := rp.parseGUID(p)
 				if err != nil {
@@ -427,6 +429,10 @@ func (rp *Parser) parseItem(p *xpp.XMLPullParser) (item *Item, err error) {
 		}
 	}
 
+	if len(enclosures) > 0 {
+		item.Enclosures = enclosures
+	}
+
 	if len(categories) > 0 {
 		item.Categories = categories
 	}
diff --git a/translator.go b/translator.go
index 6a71e3a..59bb5c8 100644
--- a/translator.go
+++ b/translator.go
@@ -437,13 +437,23 @@ func (t *DefaultRSSTranslator) translateItemCategories(rssItem *rss.Item) (categ
 }
 
 func (t *DefaultRSSTranslator) translateItemEnclosures(rssItem *rss.Item) (enclosures []*Enclosure) {
-	if rssItem.Enclosure != nil {
-		e := &Enclosure{}
-		e.URL = rssItem.Enclosure.URL
-		e.Type = rssItem.Enclosure.Type
-		e.Length = rssItem.Enclosure.Length
-		enclosures = []*Enclosure{e}
+
+	if rssItem.Enclosures != nil && len(rssItem.Enclosures) > 0 {
+
+		// Accumulate the enclosures
+		for _, enc := range rssItem.Enclosures {
+			e := &Enclosure{}
+			e.URL = enc.URL
+			e.Type = enc.Type
+			e.Length = enc.Length
+			enclosures = append(enclosures, e)
+		}
+	}
+
+	if len(enclosures) == 0 {
+		enclosures = nil
 	}
+
 	return
 }
 


Test patch:
diff --git a/testdata/parser/rss/issue_36_enclosure_text.json b/testdata/parser/rss/issue_36_enclosure_text.json
index 9798e49..5a4b3b1 100644
--- a/testdata/parser/rss/issue_36_enclosure_text.json
+++ b/testdata/parser/rss/issue_36_enclosure_text.json
@@ -5,7 +5,15 @@
                 "url": "http://example.org/podcast.mp3",
                 "length": "123456",
                 "type": "audio/mpeg"
-            }
+            },
+            "enclosures": [
+                {
+                    "url": "http://example.org/podcast.mp3",
+                    "length": "123456",
+                    "type": "audio/mpeg"
+                }
+            ]
+
         }
     ],
     "version": "2.0"
diff --git a/testdata/parser/rss/rss_channel_item_enclosure.json b/testdata/parser/rss/rss_channel_item_enclosure.json
index 9798e49..cda1a9c 100644
--- a/testdata/parser/rss/rss_channel_item_enclosure.json
+++ b/testdata/parser/rss/rss_channel_item_enclosure.json
@@ -5,7 +5,14 @@
                 "url": "http://example.org/podcast.mp3",
                 "length": "123456",
                 "type": "audio/mpeg"
-            }
+            },
+            "enclosures": [
+                {
+                    "url": "http://example.org/podcast.mp3",
+                    "length": "123456",
+                    "type": "audio/mpeg"
+                }
+            ]
         }
     ],
     "version": "2.0"
diff --git a/testdata/translator/rss/feed_item_enclosure_-_rss_channel_item_enclosure.json b/testdata/translator/rss/feed_item_enclosure_-_rss_channel_item_enclosure.json
index bfb0af3..65d5f8e 100644
--- a/testdata/translator/rss/feed_item_enclosure_-_rss_channel_item_enclosure.json
+++ b/testdata/translator/rss/feed_item_enclosure_-_rss_channel_item_enclosure.json
@@ -3,13 +3,23 @@
   "feedVersion": "2.0",
   "items": [
     {
+      "enclosure": {
+          "length": "78910",
+          "type": "audio/jpeg",
+          "url": "http://example.org/podcast.jpg"
+      },
       "enclosures": [
         {
           "length": "123456",
           "type": "audio/mpeg",
           "url": "http://example.org/podcast.mp3"
+        },
+        {
+          "length": "78910",
+          "type": "image/jpeg",
+          "url": "http://example.org/podcast.jpg"
         }
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/testdata/translator/rss/feed_item_enclosure_-_rss_channel_item_enclosure.xml b/testdata/translator/rss/feed_item_enclosure_-_rss_channel_item_enclosure.xml
index 9c63029..a8c2f93 100644
--- a/testdata/translator/rss/feed_item_enclosure_-_rss_channel_item_enclosure.xml
+++ b/testdata/translator/rss/feed_item_enclosure_-_rss_channel_item_enclosure.xml
@@ -5,6 +5,7 @@ Description: item enclosure
   <channel>
     <item>
       <enclosure url="http://example.org/podcast.mp3" length="123456" type="audio/mpeg" />
+      <enclosure url="http://example.org/podcast.jpg" length="78910" type="image/jpeg" />
     </item>
   </channel>
 </rss>
diff --git a/testdata/translator/rss/feed_item_enclosures_-_rss_channel_item_enclosures.json b/testdata/translator/rss/feed_item_enclosures_-_rss_channel_item_enclosures.json
new file mode 100644
index 0000000..7406620
--- /dev/null
+++ b/testdata/translator/rss/feed_item_enclosures_-_rss_channel_item_enclosures.json
@@ -0,0 +1,15 @@
+{
+  "feedType": "rss",
+  "feedVersion": "2.0",
+  "items": [
+    {
+      "enclosures": [
+        {
+          "length": "123456",
+          "type": "audio/mpeg",
+          "url": "http://example.org/podcast.mp3"
+        }
+      ]
+    }
+  ]
+}
diff --git a/testdata/translator/rss/feed_item_enclosures_-_rss_channel_item_enclosures.xml b/testdata/translator/rss/feed_item_enclosures_-_rss_channel_item_enclosures.xml
new file mode 100644
index 0000000..9c63029
--- /dev/null
+++ b/testdata/translator/rss/feed_item_enclosures_-_rss_channel_item_enclosures.xml
@@ -0,0 +1,10 @@
+<!--
+Description: item enclosure
+-->
+<rss version="2.0">
+  <channel>
+    <item>
+      <enclosure url="http://example.org/podcast.mp3" length="123456" type="audio/mpeg" />
+    </item>
+  </channel>
+</rss>