Add support of media elements for Atom feeds
This commit is contained in:
		
							parent
							
								
									f90e9dfab0
								
							
						
					
					
						commit
						912a98788e
					
				
					 6 changed files with 569 additions and 102 deletions
				
			
		| 
						 | 
				
			
			@ -15,6 +15,7 @@ import (
 | 
			
		|||
	"miniflux.app/logger"
 | 
			
		||||
	"miniflux.app/model"
 | 
			
		||||
	"miniflux.app/reader/date"
 | 
			
		||||
	"miniflux.app/reader/media"
 | 
			
		||||
	"miniflux.app/reader/sanitizer"
 | 
			
		||||
	"miniflux.app/url"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -29,15 +30,15 @@ type atomFeed struct {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
type atomEntry struct {
 | 
			
		||||
	ID         string         `xml:"id"`
 | 
			
		||||
	Title      atomContent    `xml:"title"`
 | 
			
		||||
	Published  string         `xml:"published"`
 | 
			
		||||
	Updated    string         `xml:"updated"`
 | 
			
		||||
	Links      []atomLink     `xml:"link"`
 | 
			
		||||
	Summary    atomContent    `xml:"summary"`
 | 
			
		||||
	Content    atomContent    `xml:"content"`
 | 
			
		||||
	MediaGroup atomMediaGroup `xml:"http://search.yahoo.com/mrss/ group"`
 | 
			
		||||
	Author     atomAuthor     `xml:"author"`
 | 
			
		||||
	ID        string      `xml:"id"`
 | 
			
		||||
	Title     atomContent `xml:"title"`
 | 
			
		||||
	Published string      `xml:"published"`
 | 
			
		||||
	Updated   string      `xml:"updated"`
 | 
			
		||||
	Links     []atomLink  `xml:"link"`
 | 
			
		||||
	Summary   atomContent `xml:"summary"`
 | 
			
		||||
	Content   atomContent `xml:"http://www.w3.org/2005/Atom content"`
 | 
			
		||||
	Author    atomAuthor  `xml:"author"`
 | 
			
		||||
	media.Element
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type atomAuthor struct {
 | 
			
		||||
| 
						 | 
				
			
			@ -58,10 +59,6 @@ type atomContent struct {
 | 
			
		|||
	XML  string `xml:",innerxml"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type atomMediaGroup struct {
 | 
			
		||||
	Description string `xml:"http://search.yahoo.com/mrss/ description"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *atomFeed) Transform() *model.Feed {
 | 
			
		||||
	feed := new(model.Feed)
 | 
			
		||||
	feed.FeedURL = getRelationURL(a.Links, "self")
 | 
			
		||||
| 
						 | 
				
			
			@ -179,8 +176,9 @@ func getContent(a *atomEntry) string {
 | 
			
		|||
		return r
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if a.MediaGroup.Description != "" {
 | 
			
		||||
		return a.MediaGroup.Description
 | 
			
		||||
	mediaDescription := a.FirstMediaDescription()
 | 
			
		||||
	if mediaDescription != "" {
 | 
			
		||||
		return mediaDescription
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return ""
 | 
			
		||||
| 
						 | 
				
			
			@ -203,11 +201,48 @@ func getHash(a *atomEntry) string {
 | 
			
		|||
 | 
			
		||||
func getEnclosures(a *atomEntry) model.EnclosureList {
 | 
			
		||||
	enclosures := make(model.EnclosureList, 0)
 | 
			
		||||
	duplicates := make(map[string]bool, 0)
 | 
			
		||||
 | 
			
		||||
	for _, mediaThumbnail := range a.AllMediaThumbnails() {
 | 
			
		||||
		if _, found := duplicates[mediaThumbnail.URL]; !found {
 | 
			
		||||
			duplicates[mediaThumbnail.URL] = true
 | 
			
		||||
			enclosures = append(enclosures, &model.Enclosure{
 | 
			
		||||
				URL:      mediaThumbnail.URL,
 | 
			
		||||
				MimeType: mediaThumbnail.MimeType(),
 | 
			
		||||
				Size:     mediaThumbnail.Size(),
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, link := range a.Links {
 | 
			
		||||
		if strings.ToLower(link.Rel) == "enclosure" {
 | 
			
		||||
			length, _ := strconv.ParseInt(link.Length, 10, 0)
 | 
			
		||||
			enclosures = append(enclosures, &model.Enclosure{URL: link.URL, MimeType: link.Type, Size: length})
 | 
			
		||||
			if _, found := duplicates[link.URL]; !found {
 | 
			
		||||
				duplicates[link.URL] = true
 | 
			
		||||
				length, _ := strconv.ParseInt(link.Length, 10, 0)
 | 
			
		||||
				enclosures = append(enclosures, &model.Enclosure{URL: link.URL, MimeType: link.Type, Size: length})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, mediaContent := range a.AllMediaContents() {
 | 
			
		||||
		if _, found := duplicates[mediaContent.URL]; !found {
 | 
			
		||||
			duplicates[mediaContent.URL] = true
 | 
			
		||||
			enclosures = append(enclosures, &model.Enclosure{
 | 
			
		||||
				URL:      mediaContent.URL,
 | 
			
		||||
				MimeType: mediaContent.MimeType(),
 | 
			
		||||
				Size:     mediaContent.Size(),
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, mediaPeerLink := range a.AllMediaPeerLinks() {
 | 
			
		||||
		if _, found := duplicates[mediaPeerLink.URL]; !found {
 | 
			
		||||
			duplicates[mediaPeerLink.URL] = true
 | 
			
		||||
			enclosures = append(enclosures, &model.Enclosure{
 | 
			
		||||
				URL:      mediaPeerLink.URL,
 | 
			
		||||
				MimeType: mediaPeerLink.MimeType(),
 | 
			
		||||
				Size:     mediaPeerLink.Size(),
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -472,31 +472,30 @@ func TestParseEntryWithEnclosures(t *testing.T) {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	if len(feed.Entries[0].Enclosures) != 2 {
 | 
			
		||||
		t.Errorf("Incorrect number of enclosures, got: %d", len(feed.Entries[0].Enclosures))
 | 
			
		||||
		t.Fatalf("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)
 | 
			
		||||
	expectedResults := []struct {
 | 
			
		||||
		url      string
 | 
			
		||||
		mimeType string
 | 
			
		||||
		size     int64
 | 
			
		||||
	}{
 | 
			
		||||
		{"http://www.example.org/myaudiofile.mp3", "audio/mpeg", 1234},
 | 
			
		||||
		{"http://www.example.org/myaudiofile.torrent", "application/x-bittorrent", 4567},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if feed.Entries[0].Enclosures[0].MimeType != "audio/mpeg" {
 | 
			
		||||
		t.Errorf("Incorrect enclosure type, got: %s", feed.Entries[0].Enclosures[0].MimeType)
 | 
			
		||||
	}
 | 
			
		||||
	for index, enclosure := range feed.Entries[0].Enclosures {
 | 
			
		||||
		if expectedResults[index].url != enclosure.URL {
 | 
			
		||||
			t.Errorf(`Unexpected enclosure URL, got %q instead of %q`, enclosure.URL, expectedResults[index].url)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	if feed.Entries[0].Enclosures[0].Size != 1234 {
 | 
			
		||||
		t.Errorf("Incorrect enclosure length, got: %d", feed.Entries[0].Enclosures[0].Size)
 | 
			
		||||
	}
 | 
			
		||||
		if expectedResults[index].mimeType != enclosure.MimeType {
 | 
			
		||||
			t.Errorf(`Unexpected enclosure type, got %q instead of %q`, enclosure.MimeType, expectedResults[index].mimeType)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	if feed.Entries[0].Enclosures[1].URL != "http://www.example.org/myaudiofile.torrent" {
 | 
			
		||||
		t.Errorf("Incorrect enclosure URL, got: %s", feed.Entries[0].Enclosures[1].URL)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if feed.Entries[0].Enclosures[1].MimeType != "application/x-bittorrent" {
 | 
			
		||||
		t.Errorf("Incorrect enclosure type, got: %s", feed.Entries[0].Enclosures[1].MimeType)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if feed.Entries[0].Enclosures[1].Size != 4567 {
 | 
			
		||||
		t.Errorf("Incorrect enclosure length, got: %d", feed.Entries[0].Enclosures[1].Size)
 | 
			
		||||
		if expectedResults[index].size != enclosure.Size {
 | 
			
		||||
			t.Errorf(`Unexpected enclosure size, got %d instead of %d`, enclosure.Size, expectedResults[index].size)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -596,3 +595,137 @@ func TestParseWithInvalidCharacterEntity(t *testing.T) {
 | 
			
		|||
		t.Errorf(`Incorrect URL, got: %q`, feed.SiteURL)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestParseMediaGroup(t *testing.T) {
 | 
			
		||||
	data := `<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
	<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
 | 
			
		||||
		<id>http://www.example.org/myfeed</id>
 | 
			
		||||
		<title>My Video Feed</title>
 | 
			
		||||
		<updated>2005-07-15T12:00:00Z</updated>
 | 
			
		||||
		<link href="http://example.org" />
 | 
			
		||||
		<link rel="self" href="http://example.org/myfeed" />
 | 
			
		||||
		<entry>
 | 
			
		||||
			<id>http://www.example.org/entries/1</id>
 | 
			
		||||
			<title>Some Video</title>
 | 
			
		||||
			<updated>2005-07-15T12:00:00Z</updated>
 | 
			
		||||
			<link href="http://www.example.org/entries/1" />
 | 
			
		||||
			<media:group>
 | 
			
		||||
				<media:title>Another title</media:title>
 | 
			
		||||
				<media:content url="https://www.youtube.com/v/abcd" type="application/x-shockwave-flash" width="640" height="390"/>
 | 
			
		||||
				<media:thumbnail url="https://example.org/thumbnail.jpg" width="480" height="360"/>
 | 
			
		||||
				<media:description>Some description
 | 
			
		||||
A website: http://example.org/</media:description>
 | 
			
		||||
			</media:group>
 | 
			
		||||
		</entry>
 | 
			
		||||
  	</feed>`
 | 
			
		||||
 | 
			
		||||
	feed, err := Parse(bytes.NewBufferString(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 feed.Entries[0].Content != `Some description<br>A website: <a href="http://example.org/">http://example.org/</a>` {
 | 
			
		||||
		t.Errorf("Incorrect entry content, got: %q", feed.Entries[0].Content)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(feed.Entries[0].Enclosures) != 2 {
 | 
			
		||||
		t.Fatalf("Incorrect number of enclosures, got: %d", len(feed.Entries[0].Enclosures))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expectedResults := []struct {
 | 
			
		||||
		url      string
 | 
			
		||||
		mimeType string
 | 
			
		||||
		size     int64
 | 
			
		||||
	}{
 | 
			
		||||
		{"https://example.org/thumbnail.jpg", "image/*", 0},
 | 
			
		||||
		{"https://www.youtube.com/v/abcd", "application/x-shockwave-flash", 0},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for index, enclosure := range feed.Entries[0].Enclosures {
 | 
			
		||||
		if expectedResults[index].url != enclosure.URL {
 | 
			
		||||
			t.Errorf(`Unexpected enclosure URL, got %q instead of %q`, enclosure.URL, expectedResults[index].url)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if expectedResults[index].mimeType != enclosure.MimeType {
 | 
			
		||||
			t.Errorf(`Unexpected enclosure type, got %q instead of %q`, enclosure.MimeType, expectedResults[index].mimeType)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if expectedResults[index].size != enclosure.Size {
 | 
			
		||||
			t.Errorf(`Unexpected enclosure size, got %d instead of %d`, enclosure.Size, expectedResults[index].size)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestParseMediaElements(t *testing.T) {
 | 
			
		||||
	data := `<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
	<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
 | 
			
		||||
		<id>http://www.example.org/myfeed</id>
 | 
			
		||||
		<title>My Video Feed</title>
 | 
			
		||||
		<updated>2005-07-15T12:00:00Z</updated>
 | 
			
		||||
		<link href="http://example.org" />
 | 
			
		||||
		<link rel="self" href="http://example.org/myfeed" />
 | 
			
		||||
		<entry>
 | 
			
		||||
			<id>http://www.example.org/entries/1</id>
 | 
			
		||||
			<title>Some Video</title>
 | 
			
		||||
			<updated>2005-07-15T12:00:00Z</updated>
 | 
			
		||||
			<link href="http://www.example.org/entries/1" />
 | 
			
		||||
			<media:title>Another title</media:title>
 | 
			
		||||
			<media:content url="https://www.youtube.com/v/abcd" type="application/x-shockwave-flash" width="640" height="390"/>
 | 
			
		||||
			<media:thumbnail url="https://example.org/thumbnail.jpg" width="480" height="360"/>
 | 
			
		||||
			<media:description>Some description
 | 
			
		||||
A website: http://example.org/</media:description>
 | 
			
		||||
		</entry>
 | 
			
		||||
  	</feed>`
 | 
			
		||||
 | 
			
		||||
	feed, err := Parse(bytes.NewBufferString(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 feed.Entries[0].Content != `Some description<br>A website: <a href="http://example.org/">http://example.org/</a>` {
 | 
			
		||||
		t.Errorf("Incorrect entry content, got: %q", feed.Entries[0].Content)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(feed.Entries[0].Enclosures) != 2 {
 | 
			
		||||
		t.Fatalf("Incorrect number of enclosures, got: %d", len(feed.Entries[0].Enclosures))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expectedResults := []struct {
 | 
			
		||||
		url      string
 | 
			
		||||
		mimeType string
 | 
			
		||||
		size     int64
 | 
			
		||||
	}{
 | 
			
		||||
		{"https://example.org/thumbnail.jpg", "image/*", 0},
 | 
			
		||||
		{"https://www.youtube.com/v/abcd", "application/x-shockwave-flash", 0},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for index, enclosure := range feed.Entries[0].Enclosures {
 | 
			
		||||
		if expectedResults[index].url != enclosure.URL {
 | 
			
		||||
			t.Errorf(`Unexpected enclosure URL, got %q instead of %q`, enclosure.URL, expectedResults[index].url)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if expectedResults[index].mimeType != enclosure.MimeType {
 | 
			
		||||
			t.Errorf(`Unexpected enclosure type, got %q instead of %q`, enclosure.MimeType, expectedResults[index].mimeType)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if expectedResults[index].size != enclosure.Size {
 | 
			
		||||
			t.Errorf(`Unexpected enclosure size, got %d instead of %d`, enclosure.Size, expectedResults[index].size)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										176
									
								
								reader/media/media.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								reader/media/media.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,176 @@
 | 
			
		|||
// Copyright 2019 Frédéric Guillot. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by the Apache 2.0
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package media // import "miniflux.app/reader/media"
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var textLinkRegex = regexp.MustCompile(`(?mi)(\bhttps?:\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])`)
 | 
			
		||||
 | 
			
		||||
// Element represents XML media elements.
 | 
			
		||||
type Element 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"`
 | 
			
		||||
	MediaDescriptions DescriptionList `xml:"http://search.yahoo.com/mrss/ description"`
 | 
			
		||||
	MediaPeerLinks    []PeerLink      `xml:"http://search.yahoo.com/mrss/ peerLink"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AllMediaThumbnails returns all thumbnail elements merged together.
 | 
			
		||||
func (e *Element) AllMediaThumbnails() []Thumbnail {
 | 
			
		||||
	var items []Thumbnail
 | 
			
		||||
	items = append(items, e.MediaThumbnails...)
 | 
			
		||||
	for _, mediaGroup := range e.MediaGroups {
 | 
			
		||||
		items = append(items, mediaGroup.MediaThumbnails...)
 | 
			
		||||
	}
 | 
			
		||||
	return items
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AllMediaContents returns all content elements merged together.
 | 
			
		||||
func (e *Element) AllMediaContents() []Content {
 | 
			
		||||
	var items []Content
 | 
			
		||||
	items = append(items, e.MediaContents...)
 | 
			
		||||
	for _, mediaGroup := range e.MediaGroups {
 | 
			
		||||
		items = append(items, mediaGroup.MediaContents...)
 | 
			
		||||
	}
 | 
			
		||||
	return items
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AllMediaPeerLinks returns all peer link elements merged together.
 | 
			
		||||
func (e *Element) AllMediaPeerLinks() []PeerLink {
 | 
			
		||||
	var items []PeerLink
 | 
			
		||||
	items = append(items, e.MediaPeerLinks...)
 | 
			
		||||
	for _, mediaGroup := range e.MediaGroups {
 | 
			
		||||
		items = append(items, mediaGroup.MediaPeerLinks...)
 | 
			
		||||
	}
 | 
			
		||||
	return items
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FirstMediaDescription returns the first description element.
 | 
			
		||||
func (e *Element) FirstMediaDescription() string {
 | 
			
		||||
	description := e.MediaDescriptions.First()
 | 
			
		||||
	if description != "" {
 | 
			
		||||
		return description
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, mediaGroup := range e.MediaGroups {
 | 
			
		||||
		description = mediaGroup.MediaDescriptions.First()
 | 
			
		||||
		if description != "" {
 | 
			
		||||
			return description
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Group represents a XML element "media:group".
 | 
			
		||||
type Group struct {
 | 
			
		||||
	MediaContents     []Content       `xml:"http://search.yahoo.com/mrss/ content"`
 | 
			
		||||
	MediaThumbnails   []Thumbnail     `xml:"http://search.yahoo.com/mrss/ thumbnail"`
 | 
			
		||||
	MediaDescriptions DescriptionList `xml:"http://search.yahoo.com/mrss/ description"`
 | 
			
		||||
	MediaPeerLinks    []PeerLink      `xml:"http://search.yahoo.com/mrss/ peerLink"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Content represents a XML element "media:content".
 | 
			
		||||
type Content struct {
 | 
			
		||||
	URL      string `xml:"url,attr"`
 | 
			
		||||
	Type     string `xml:"type,attr"`
 | 
			
		||||
	FileSize string `xml:"fileSize,attr"`
 | 
			
		||||
	Medium   string `xml:"medium,attr"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MimeType returns the attachment mime type.
 | 
			
		||||
func (mc *Content) MimeType() string {
 | 
			
		||||
	switch {
 | 
			
		||||
	case mc.Type == "" && mc.Medium == "image":
 | 
			
		||||
		return "image/*"
 | 
			
		||||
	case mc.Type == "" && mc.Medium == "video":
 | 
			
		||||
		return "video/*"
 | 
			
		||||
	case mc.Type == "" && mc.Medium == "audio":
 | 
			
		||||
		return "audio/*"
 | 
			
		||||
	case mc.Type == "" && mc.Medium == "video":
 | 
			
		||||
		return "video/*"
 | 
			
		||||
	case mc.Type != "":
 | 
			
		||||
		return mc.Type
 | 
			
		||||
	default:
 | 
			
		||||
		return "application/octet-stream"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Size returns the attachment size.
 | 
			
		||||
func (mc *Content) Size() int64 {
 | 
			
		||||
	if mc.FileSize == "" {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
	size, _ := strconv.ParseInt(mc.FileSize, 10, 0)
 | 
			
		||||
	return size
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Thumbnail represents a XML element "media:thumbnail".
 | 
			
		||||
type Thumbnail struct {
 | 
			
		||||
	URL string `xml:"url,attr"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MimeType returns the attachment mime type.
 | 
			
		||||
func (t *Thumbnail) MimeType() string {
 | 
			
		||||
	return "image/*"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Size returns the attachment size.
 | 
			
		||||
func (t *Thumbnail) Size() int64 {
 | 
			
		||||
	return 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PeerLink represents a XML element "media:peerLink".
 | 
			
		||||
type PeerLink struct {
 | 
			
		||||
	URL  string `xml:"href,attr"`
 | 
			
		||||
	Type string `xml:"type,attr"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MimeType returns the attachment mime type.
 | 
			
		||||
func (p *PeerLink) MimeType() string {
 | 
			
		||||
	if p.Type != "" {
 | 
			
		||||
		return p.Type
 | 
			
		||||
	}
 | 
			
		||||
	return "application/octet-stream"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Size returns the attachment size.
 | 
			
		||||
func (p *PeerLink) Size() int64 {
 | 
			
		||||
	return 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Description represents a XML element "media:description".
 | 
			
		||||
type Description struct {
 | 
			
		||||
	Type        string `xml:"type,attr"`
 | 
			
		||||
	Description string `xml:",chardata"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HTML returns the description as HTML.
 | 
			
		||||
func (d *Description) HTML() string {
 | 
			
		||||
	if d.Type == "html" {
 | 
			
		||||
		return d.Description
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	content := strings.Replace(d.Description, "\n", "<br>", -1)
 | 
			
		||||
	return textLinkRegex.ReplaceAllString(content, `<a href="${1}">${1}</a>`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DescriptionList represents a list of "media:description" XML elements.
 | 
			
		||||
type DescriptionList []Description
 | 
			
		||||
 | 
			
		||||
// First returns the first non-empty description.
 | 
			
		||||
func (dl DescriptionList) First() string {
 | 
			
		||||
	for _, description := range dl {
 | 
			
		||||
		contents := description.HTML()
 | 
			
		||||
		if contents != "" {
 | 
			
		||||
			return contents
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										110
									
								
								reader/media/media_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								reader/media/media_test.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,110 @@
 | 
			
		|||
// Copyright 2019 Frédéric Guillot. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by the Apache 2.0
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package media // import "miniflux.app/reader/media"
 | 
			
		||||
 | 
			
		||||
import "testing"
 | 
			
		||||
 | 
			
		||||
func TestContentMimeType(t *testing.T) {
 | 
			
		||||
	scenarios := []struct {
 | 
			
		||||
		inputType, inputMedium, expectedMimeType string
 | 
			
		||||
	}{
 | 
			
		||||
		{"image/png", "image", "image/png"},
 | 
			
		||||
		{"", "image", "image/*"},
 | 
			
		||||
		{"", "video", "video/*"},
 | 
			
		||||
		{"", "audio", "audio/*"},
 | 
			
		||||
		{"", "", "application/octet-stream"},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, scenario := range scenarios {
 | 
			
		||||
		content := &Content{Type: scenario.inputType, Medium: scenario.inputMedium}
 | 
			
		||||
		result := content.MimeType()
 | 
			
		||||
		if result != scenario.expectedMimeType {
 | 
			
		||||
			t.Errorf(`Unexpected mime type, got %q instead of %q for type=%q medium=%q`,
 | 
			
		||||
				result,
 | 
			
		||||
				scenario.expectedMimeType,
 | 
			
		||||
				scenario.inputType,
 | 
			
		||||
				scenario.inputMedium,
 | 
			
		||||
			)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestContentSize(t *testing.T) {
 | 
			
		||||
	scenarios := []struct {
 | 
			
		||||
		inputSize    string
 | 
			
		||||
		expectedSize int64
 | 
			
		||||
	}{
 | 
			
		||||
		{"", 0},
 | 
			
		||||
		{"123", int64(123)},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, scenario := range scenarios {
 | 
			
		||||
		content := &Content{FileSize: scenario.inputSize}
 | 
			
		||||
		result := content.Size()
 | 
			
		||||
		if result != scenario.expectedSize {
 | 
			
		||||
			t.Errorf(`Unexpected size, got %d instead of %d for %q`,
 | 
			
		||||
				result,
 | 
			
		||||
				scenario.expectedSize,
 | 
			
		||||
				scenario.inputSize,
 | 
			
		||||
			)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestPeerLinkType(t *testing.T) {
 | 
			
		||||
	scenarios := []struct {
 | 
			
		||||
		inputType        string
 | 
			
		||||
		expectedMimeType string
 | 
			
		||||
	}{
 | 
			
		||||
		{"", "application/octet-stream"},
 | 
			
		||||
		{"application/x-bittorrent", "application/x-bittorrent"},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, scenario := range scenarios {
 | 
			
		||||
		peerLink := &PeerLink{Type: scenario.inputType}
 | 
			
		||||
		result := peerLink.MimeType()
 | 
			
		||||
		if result != scenario.expectedMimeType {
 | 
			
		||||
			t.Errorf(`Unexpected mime type, got %q instead of %q for %q`,
 | 
			
		||||
				result,
 | 
			
		||||
				scenario.expectedMimeType,
 | 
			
		||||
				scenario.inputType,
 | 
			
		||||
			)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDescription(t *testing.T) {
 | 
			
		||||
	scenarios := []struct {
 | 
			
		||||
		inputType           string
 | 
			
		||||
		inputContent        string
 | 
			
		||||
		expectedDescription string
 | 
			
		||||
	}{
 | 
			
		||||
		{"", "", ""},
 | 
			
		||||
		{"html", "a <b>c</b>", "a <b>c</b>"},
 | 
			
		||||
		{"plain", "a\nhttp://www.example.org/", `a<br><a href="http://www.example.org/">http://www.example.org/</a>`},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, scenario := range scenarios {
 | 
			
		||||
		desc := &Description{Type: scenario.inputType, Description: scenario.inputContent}
 | 
			
		||||
		result := desc.HTML()
 | 
			
		||||
		if result != scenario.expectedDescription {
 | 
			
		||||
			t.Errorf(`Unexpected description, got %q instead of %q for %q`,
 | 
			
		||||
				result,
 | 
			
		||||
				scenario.expectedDescription,
 | 
			
		||||
				scenario.inputType,
 | 
			
		||||
			)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestFirstDescription(t *testing.T) {
 | 
			
		||||
	var descList DescriptionList
 | 
			
		||||
	descList = append(descList, Description{})
 | 
			
		||||
	descList = append(descList, Description{Description: "Something"})
 | 
			
		||||
 | 
			
		||||
	if descList.First() != "Something" {
 | 
			
		||||
		t.Errorf(`Unexpected description`)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -771,3 +771,52 @@ func TestParseEntryWithMediaContent(t *testing.T) {
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestParseEntryWithMediaPeerLink(t *testing.T) {
 | 
			
		||||
	data := `<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
		<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/">
 | 
			
		||||
		<channel>
 | 
			
		||||
		<title>My Example Feed</title>
 | 
			
		||||
		<link>http://example.org</link>
 | 
			
		||||
		<item>
 | 
			
		||||
			<title>Example Item</title>
 | 
			
		||||
			<link>http://www.example.org/entries/1</link>
 | 
			
		||||
			<media:peerLink type="application/x-bittorrent" href="http://www.example.org/file.torrent" />
 | 
			
		||||
		</item>
 | 
			
		||||
		</channel>
 | 
			
		||||
		</rss>`
 | 
			
		||||
 | 
			
		||||
	feed, err := Parse(bytes.NewBufferString(data))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(feed.Entries) != 1 {
 | 
			
		||||
		t.Errorf("Incorrect number of entries, got: %d", len(feed.Entries))
 | 
			
		||||
	}
 | 
			
		||||
	if len(feed.Entries[0].Enclosures) != 1 {
 | 
			
		||||
		t.Fatalf("Incorrect number of enclosures, got: %d", len(feed.Entries[0].Enclosures))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expectedResults := []struct {
 | 
			
		||||
		url      string
 | 
			
		||||
		mimeType string
 | 
			
		||||
		size     int64
 | 
			
		||||
	}{
 | 
			
		||||
		{"http://www.example.org/file.torrent", "application/x-bittorrent", 0},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for index, enclosure := range feed.Entries[0].Enclosures {
 | 
			
		||||
		if expectedResults[index].url != enclosure.URL {
 | 
			
		||||
			t.Errorf(`Unexpected enclosure URL, got %q instead of %q`, enclosure.URL, expectedResults[index].url)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if expectedResults[index].mimeType != enclosure.MimeType {
 | 
			
		||||
			t.Errorf(`Unexpected enclosure type, got %q instead of %q`, enclosure.MimeType, expectedResults[index].mimeType)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if expectedResults[index].size != enclosure.Size {
 | 
			
		||||
			t.Errorf(`Unexpected enclosure size, got %d instead of %d`, enclosure.Size, expectedResults[index].size)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,6 +15,7 @@ import (
 | 
			
		|||
	"miniflux.app/logger"
 | 
			
		||||
	"miniflux.app/model"
 | 
			
		||||
	"miniflux.app/reader/date"
 | 
			
		||||
	"miniflux.app/reader/media"
 | 
			
		||||
	"miniflux.app/reader/sanitizer"
 | 
			
		||||
	"miniflux.app/url"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -65,62 +66,20 @@ func (enclosure *rssEnclosure) Size() int64 {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
type rssItem struct {
 | 
			
		||||
	GUID              string               `xml:"guid"`
 | 
			
		||||
	Title             string               `xml:"title"`
 | 
			
		||||
	Links             []rssLink            `xml:"link"`
 | 
			
		||||
	OriginalLink      string               `xml:"http://rssnamespace.org/feedburner/ext/1.0 origLink"`
 | 
			
		||||
	CommentLinks      []rssCommentLink     `xml:"comments"`
 | 
			
		||||
	Description       string               `xml:"description"`
 | 
			
		||||
	EncodedContent    string               `xml:"http://purl.org/rss/1.0/modules/content/ encoded"`
 | 
			
		||||
	PubDate           string               `xml:"pubDate"`
 | 
			
		||||
	Date              string               `xml:"http://purl.org/dc/elements/1.1/ date"`
 | 
			
		||||
	Authors           []rssAuthor          `xml:"author"`
 | 
			
		||||
	Creator           string               `xml:"http://purl.org/dc/elements/1.1/ creator"`
 | 
			
		||||
	EnclosureLinks    []rssEnclosure       `xml:"enclosure"`
 | 
			
		||||
	OrigEnclosureLink string               `xml:"http://rssnamespace.org/feedburner/ext/1.0 origEnclosureLink"`
 | 
			
		||||
	MediaGroup        []rssMediaGroup      `xml:"http://search.yahoo.com/mrss/ group"`
 | 
			
		||||
	MediaContents     []rssMediaContent    `xml:"http://search.yahoo.com/mrss/ content"`
 | 
			
		||||
	MediaThumbnails   []rssMediaThumbnails `xml:"http://search.yahoo.com/mrss/ thumbnail"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type rssMediaGroup struct {
 | 
			
		||||
	MediaList []rssMediaContent `xml:"content"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type rssMediaContent struct {
 | 
			
		||||
	URL      string `xml:"url,attr"`
 | 
			
		||||
	Type     string `xml:"type,attr"`
 | 
			
		||||
	FileSize string `xml:"fileSize,attr"`
 | 
			
		||||
	Medium   string `xml:"medium,attr"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (mediaContent *rssMediaContent) MimeType() string {
 | 
			
		||||
	switch {
 | 
			
		||||
	case mediaContent.Type == "" && mediaContent.Medium == "image":
 | 
			
		||||
		return "image/*"
 | 
			
		||||
	case mediaContent.Type == "" && mediaContent.Medium == "video":
 | 
			
		||||
		return "video/*"
 | 
			
		||||
	case mediaContent.Type == "" && mediaContent.Medium == "audio":
 | 
			
		||||
		return "audio/*"
 | 
			
		||||
	case mediaContent.Type == "" && mediaContent.Medium == "video":
 | 
			
		||||
		return "video/*"
 | 
			
		||||
	case mediaContent.Type != "":
 | 
			
		||||
		return mediaContent.Type
 | 
			
		||||
	default:
 | 
			
		||||
		return "application/octet-stream"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (mediaContent *rssMediaContent) Size() int64 {
 | 
			
		||||
	if mediaContent.FileSize == "" {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
	size, _ := strconv.ParseInt(mediaContent.FileSize, 10, 0)
 | 
			
		||||
	return size
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type rssMediaThumbnails struct {
 | 
			
		||||
	URL string `xml:"url,attr"`
 | 
			
		||||
	GUID              string           `xml:"guid"`
 | 
			
		||||
	Title             string           `xml:"title"`
 | 
			
		||||
	Links             []rssLink        `xml:"link"`
 | 
			
		||||
	OriginalLink      string           `xml:"http://rssnamespace.org/feedburner/ext/1.0 origLink"`
 | 
			
		||||
	CommentLinks      []rssCommentLink `xml:"comments"`
 | 
			
		||||
	Description       string           `xml:"description"`
 | 
			
		||||
	EncodedContent    string           `xml:"http://purl.org/rss/1.0/modules/content/ encoded"`
 | 
			
		||||
	PubDate           string           `xml:"pubDate"`
 | 
			
		||||
	Date              string           `xml:"http://purl.org/dc/elements/1.1/ date"`
 | 
			
		||||
	Authors           []rssAuthor      `xml:"author"`
 | 
			
		||||
	Creator           string           `xml:"http://purl.org/dc/elements/1.1/ creator"`
 | 
			
		||||
	EnclosureLinks    []rssEnclosure   `xml:"enclosure"`
 | 
			
		||||
	OrigEnclosureLink string           `xml:"http://rssnamespace.org/feedburner/ext/1.0 origEnclosureLink"`
 | 
			
		||||
	media.Element
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *rssFeed) SiteURL() string {
 | 
			
		||||
| 
						 | 
				
			
			@ -253,13 +212,13 @@ func (r *rssItem) Enclosures() model.EnclosureList {
 | 
			
		|||
	enclosures := make(model.EnclosureList, 0)
 | 
			
		||||
	duplicates := make(map[string]bool, 0)
 | 
			
		||||
 | 
			
		||||
	for _, mediaThumbnail := range r.MediaThumbnails {
 | 
			
		||||
	for _, mediaThumbnail := range r.AllMediaThumbnails() {
 | 
			
		||||
		if _, found := duplicates[mediaThumbnail.URL]; !found {
 | 
			
		||||
			duplicates[mediaThumbnail.URL] = true
 | 
			
		||||
			enclosures = append(enclosures, &model.Enclosure{
 | 
			
		||||
				URL:      mediaThumbnail.URL,
 | 
			
		||||
				MimeType: "image/*",
 | 
			
		||||
				Size:     0,
 | 
			
		||||
				MimeType: mediaThumbnail.MimeType(),
 | 
			
		||||
				Size:     mediaThumbnail.Size(),
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -285,13 +244,7 @@ func (r *rssItem) Enclosures() model.EnclosureList {
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, mediaContentItem := range r.MediaGroup {
 | 
			
		||||
		for _, mediaContent := range mediaContentItem.MediaList {
 | 
			
		||||
			r.MediaContents = append(r.MediaContents, mediaContent)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, mediaContent := range r.MediaContents {
 | 
			
		||||
	for _, mediaContent := range r.AllMediaContents() {
 | 
			
		||||
		if _, found := duplicates[mediaContent.URL]; !found {
 | 
			
		||||
			duplicates[mediaContent.URL] = true
 | 
			
		||||
			enclosures = append(enclosures, &model.Enclosure{
 | 
			
		||||
| 
						 | 
				
			
			@ -302,6 +255,17 @@ func (r *rssItem) Enclosures() model.EnclosureList {
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, mediaPeerLink := range r.AllMediaPeerLinks() {
 | 
			
		||||
		if _, found := duplicates[mediaPeerLink.URL]; !found {
 | 
			
		||||
			duplicates[mediaPeerLink.URL] = true
 | 
			
		||||
			enclosures = append(enclosures, &model.Enclosure{
 | 
			
		||||
				URL:      mediaPeerLink.URL,
 | 
			
		||||
				MimeType: mediaPeerLink.MimeType(),
 | 
			
		||||
				Size:     mediaPeerLink.Size(),
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return enclosures
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue