Ver código fonte

refactor(reader): make code flow consistent for buildFeed

Every build feed method does similar things in a different way. That makes it harder to read these implementations.

Remove nesting creep in loops to make code simpler.
gudvinr 3 semanas atrás
pai
commit
4ce8151a8f

+ 16 - 9
internal/reader/atom/atom_03_adapter.go

@@ -19,7 +19,10 @@ type atom03Adapter struct {
 }
 
 func (a *atom03Adapter) buildFeed(baseURL string) *model.Feed {
-	feed := new(model.Feed)
+	feed := &model.Feed{
+		FeedURL: baseURL,
+		SiteURL: baseURL,
+	}
 
 	// Populate the feed URL.
 	feedURL := a.atomFeed.Links.firstLinkWithRelation("self")
@@ -27,8 +30,6 @@ func (a *atom03Adapter) buildFeed(baseURL string) *model.Feed {
 		if absoluteFeedURL, err := urllib.ResolveToAbsoluteURL(baseURL, feedURL); err == nil {
 			feed.FeedURL = absoluteFeedURL
 		}
-	} else {
-		feed.FeedURL = baseURL
 	}
 
 	// Populate the site URL.
@@ -37,8 +38,6 @@ func (a *atom03Adapter) buildFeed(baseURL string) *model.Feed {
 		if absoluteSiteURL, err := urllib.ResolveToAbsoluteURL(baseURL, siteURL); err == nil {
 			feed.SiteURL = absoluteSiteURL
 		}
-	} else {
-		feed.SiteURL = baseURL
 	}
 
 	// Populate the feed title.
@@ -69,6 +68,7 @@ func (a *atom03Adapter) buildFeed(baseURL string) *model.Feed {
 		if entry.Title == "" {
 			entry.Title = sanitizer.TruncateHTML(entry.Content, 100)
 		}
+
 		if entry.Title == "" {
 			entry.Title = entry.URL
 		}
@@ -81,17 +81,24 @@ func (a *atom03Adapter) buildFeed(baseURL string) *model.Feed {
 
 		// Populate the entry date.
 		for _, value := range []string{atomEntry.Issued, atomEntry.Modified, atomEntry.Created} {
-			if parsedDate, err := date.Parse(value); err == nil {
-				entry.Date = parsedDate
-				break
-			} else {
+			if value = strings.TrimSpace(value); value == "" {
+				continue
+			}
+
+			parsedDate, err := date.Parse(value)
+			if err != nil {
 				slog.Debug("Unable to parse date from Atom 0.3 feed",
 					slog.String("date", value),
 					slog.String("id", atomEntry.ID),
 					slog.Any("error", err),
 				)
+				continue
 			}
+
+			entry.Date = parsedDate
+			break
 		}
+
 		if entry.Date.IsZero() {
 			entry.Date = time.Now()
 		}

+ 95 - 67
internal/reader/atom/atom_10_adapter.go

@@ -27,7 +27,10 @@ func NewAtom10Adapter(atomFeed *atom10Feed) *atom10Adapter {
 }
 
 func (a *atom10Adapter) BuildFeed(baseURL string) *model.Feed {
-	feed := new(model.Feed)
+	feed := &model.Feed{
+		FeedURL: baseURL,
+		SiteURL: baseURL,
+	}
 
 	// Populate the feed URL.
 	feedURL := a.atomFeed.Links.firstLinkWithRelation("self")
@@ -35,8 +38,6 @@ func (a *atom10Adapter) BuildFeed(baseURL string) *model.Feed {
 		if absoluteFeedURL, err := urllib.ResolveToAbsoluteURL(baseURL, feedURL); err == nil {
 			feed.FeedURL = absoluteFeedURL
 		}
-	} else {
-		feed.FeedURL = baseURL
 	}
 
 	// Populate the site URL.
@@ -45,8 +46,6 @@ func (a *atom10Adapter) BuildFeed(baseURL string) *model.Feed {
 		if absoluteSiteURL, err := urllib.ResolveToAbsoluteURL(baseURL, siteURL); err == nil {
 			feed.SiteURL = absoluteSiteURL
 		}
-	} else {
-		feed.SiteURL = baseURL
 	}
 
 	// Populate the feed title.
@@ -59,15 +58,17 @@ func (a *atom10Adapter) BuildFeed(baseURL string) *model.Feed {
 	feed.Description = a.atomFeed.Subtitle.body()
 
 	// Populate the feed icon.
-	if a.atomFeed.Icon != "" {
-		if absoluteIconURL, err := urllib.ResolveToAbsoluteURL(feed.SiteURL, a.atomFeed.Icon); err == nil {
-			feed.IconURL = absoluteIconURL
+	for _, value := range []string{a.atomFeed.Icon, a.atomFeed.Logo} {
+		if value = strings.TrimSpace(value); value == "" {
+			continue
 		}
-	} else if a.atomFeed.Logo != "" {
-		if absoluteLogoURL, err := urllib.ResolveToAbsoluteURL(feed.SiteURL, a.atomFeed.Logo); err == nil {
-			feed.IconURL = absoluteLogoURL
+
+		if iconURL, err := urllib.ResolveToAbsoluteURL(feed.SiteURL, value); err == nil {
+			feed.IconURL = iconURL
+			break
 		}
 	}
+
 	feed.Entries = a.populateEntries(feed.SiteURL)
 	return feed
 }
@@ -115,19 +116,24 @@ func (a *atom10Adapter) populateEntries(siteURL string) model.Entries {
 
 		// Populate the entry date.
 		for _, value := range []string{atomEntry.Published, atomEntry.Updated} {
-			if value != "" {
-				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 value = strings.TrimSpace(value); value == "" {
+				continue
+			}
+
+			parsedDate, err := date.Parse(value)
+			if 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),
+				)
+				continue
 			}
+
+			entry.Date = parsedDate
+			break
 		}
+
 		if entry.Date.IsZero() {
 			entry.Date = time.Now()
 		}
@@ -167,22 +173,28 @@ func (a *atom10Adapter) populateEntries(siteURL string) model.Entries {
 			if mediaURL == "" {
 				continue
 			}
-			if _, found := uniqueEnclosuresMap[mediaURL]; !found {
-				if mediaAbsoluteURL, err := urllib.ResolveToAbsoluteURL(siteURL, mediaURL); err != nil {
-					slog.Debug("Unable to build absolute URL for media thumbnail",
-						slog.String("url", mediaThumbnail.URL),
-						slog.String("site_url", siteURL),
-						slog.Any("error", err),
-					)
-				} else {
-					uniqueEnclosuresMap[mediaAbsoluteURL] = true
-					entry.Enclosures = append(entry.Enclosures, &model.Enclosure{
-						URL:      mediaAbsoluteURL,
-						MimeType: mediaThumbnail.MimeType(),
-						Size:     mediaThumbnail.Size(),
-					})
-				}
+
+			if _, found := uniqueEnclosuresMap[mediaURL]; found {
+				continue
 			}
+
+			mediaAbsoluteURL, err := urllib.ResolveToAbsoluteURL(siteURL, mediaURL)
+			if err != nil {
+				slog.Debug("Unable to build absolute URL for media thumbnail",
+					slog.String("url", mediaThumbnail.URL),
+					slog.String("site_url", siteURL),
+					slog.Any("error", err),
+				)
+				continue
+			}
+
+			uniqueEnclosuresMap[mediaAbsoluteURL] = true
+
+			entry.Enclosures = append(entry.Enclosures, &model.Enclosure{
+				URL:      mediaAbsoluteURL,
+				MimeType: mediaThumbnail.MimeType(),
+				Size:     mediaThumbnail.Size(),
+			})
 		}
 
 		for _, link := range atomEntry.Links.findAllLinksWithRelation("enclosure") {
@@ -193,17 +205,21 @@ func (a *atom10Adapter) populateEntries(siteURL string) model.Entries {
 					slog.String("entry_url", entry.URL),
 					slog.Any("error", err),
 				)
-			} else {
-				if _, found := uniqueEnclosuresMap[absoluteEnclosureURL]; !found {
-					uniqueEnclosuresMap[absoluteEnclosureURL] = true
-					length, _ := strconv.ParseInt(link.Length, 10, 0)
-					entry.Enclosures = append(entry.Enclosures, &model.Enclosure{
-						URL:      absoluteEnclosureURL,
-						MimeType: link.Type,
-						Size:     length,
-					})
-				}
+				continue
+			}
+
+			if _, found := uniqueEnclosuresMap[absoluteEnclosureURL]; found {
+				continue
 			}
+
+			uniqueEnclosuresMap[absoluteEnclosureURL] = true
+
+			length, _ := strconv.ParseInt(link.Length, 10, 0)
+			entry.Enclosures = append(entry.Enclosures, &model.Enclosure{
+				URL:      absoluteEnclosureURL,
+				MimeType: link.Type,
+				Size:     length,
+			})
 		}
 
 		for _, mediaContent := range atomEntry.AllMediaContents() {
@@ -211,22 +227,28 @@ func (a *atom10Adapter) populateEntries(siteURL string) model.Entries {
 			if mediaURL == "" {
 				continue
 			}
-			if mediaAbsoluteURL, err := urllib.ResolveToAbsoluteURL(siteURL, mediaURL); err != nil {
+
+			mediaAbsoluteURL, err := urllib.ResolveToAbsoluteURL(siteURL, mediaURL)
+			if err != nil {
 				slog.Debug("Unable to build absolute URL for media content",
 					slog.String("url", mediaContent.URL),
 					slog.String("site_url", siteURL),
 					slog.Any("error", err),
 				)
-			} else {
-				if _, found := uniqueEnclosuresMap[mediaAbsoluteURL]; !found {
-					uniqueEnclosuresMap[mediaAbsoluteURL] = true
-					entry.Enclosures = append(entry.Enclosures, &model.Enclosure{
-						URL:      mediaAbsoluteURL,
-						MimeType: mediaContent.MimeType(),
-						Size:     mediaContent.Size(),
-					})
-				}
+				continue
 			}
+
+			if _, found := uniqueEnclosuresMap[mediaAbsoluteURL]; found {
+				continue
+			}
+
+			uniqueEnclosuresMap[mediaAbsoluteURL] = true
+
+			entry.Enclosures = append(entry.Enclosures, &model.Enclosure{
+				URL:      mediaAbsoluteURL,
+				MimeType: mediaContent.MimeType(),
+				Size:     mediaContent.Size(),
+			})
 		}
 
 		for _, mediaPeerLink := range atomEntry.AllMediaPeerLinks() {
@@ -234,22 +256,28 @@ func (a *atom10Adapter) populateEntries(siteURL string) model.Entries {
 			if mediaURL == "" {
 				continue
 			}
-			if mediaAbsoluteURL, err := urllib.ResolveToAbsoluteURL(siteURL, mediaURL); err != nil {
+
+			mediaAbsoluteURL, err := urllib.ResolveToAbsoluteURL(siteURL, mediaURL)
+			if err != nil {
 				slog.Debug("Unable to build absolute URL for media peer link",
 					slog.String("url", mediaPeerLink.URL),
 					slog.String("site_url", siteURL),
 					slog.Any("error", err),
 				)
-			} else {
-				if _, found := uniqueEnclosuresMap[mediaAbsoluteURL]; !found {
-					uniqueEnclosuresMap[mediaAbsoluteURL] = true
-					entry.Enclosures = append(entry.Enclosures, &model.Enclosure{
-						URL:      mediaAbsoluteURL,
-						MimeType: mediaPeerLink.MimeType(),
-						Size:     mediaPeerLink.Size(),
-					})
-				}
+				continue
 			}
+
+			if _, found := uniqueEnclosuresMap[mediaAbsoluteURL]; found {
+				continue
+			}
+
+			uniqueEnclosuresMap[mediaAbsoluteURL] = true
+
+			entry.Enclosures = append(entry.Enclosures, &model.Enclosure{
+				URL:      mediaAbsoluteURL,
+				MimeType: mediaPeerLink.MimeType(),
+				Size:     mediaPeerLink.Size(),
+			})
 		}
 
 		entries = append(entries, entry)

+ 55 - 38
internal/reader/json/adapter.go

@@ -56,32 +56,34 @@ func (j *JSONAdapter) BuildFeed(baseURL string) *model.Feed {
 
 	// Populate the icon URL if present.
 	for _, iconURL := range []string{j.jsonFeed.FaviconURL, j.jsonFeed.IconURL} {
-		iconURL = strings.TrimSpace(iconURL)
-		if iconURL != "" {
-			if absoluteIconURL, err := urllib.ResolveToAbsoluteURL(feed.SiteURL, iconURL); err == nil {
-				feed.IconURL = absoluteIconURL
-				break
-			}
+		if iconURL = strings.TrimSpace(iconURL); iconURL == "" {
+			continue
+		}
+
+		if absoluteIconURL, err := urllib.ResolveToAbsoluteURL(feed.SiteURL, iconURL); err == nil {
+			feed.IconURL = absoluteIconURL
+			break
 		}
 	}
 
 	for _, item := range j.jsonFeed.Items {
 		entry := model.NewEntry()
-		entry.Title = strings.TrimSpace(item.Title)
 
 		for _, itemURL := range []string{item.URL, item.ExternalURL} {
-			itemURL = strings.TrimSpace(itemURL)
-			if itemURL != "" {
-				// Make sure the entry URL is absolute.
-				if entryURL, err := urllib.ResolveToAbsoluteURL(feed.SiteURL, itemURL); err == nil {
-					entry.URL = entryURL
-				}
+			if itemURL = strings.TrimSpace(itemURL); itemURL == "" {
+				continue
+			}
+
+			// Make sure the entry URL is absolute.
+			if entryURL, err := urllib.ResolveToAbsoluteURL(feed.SiteURL, itemURL); err == nil {
+				entry.URL = entryURL
 				break
 			}
 		}
 
-		// The entry title is optional, so we need to find a fallback.
+		entry.Title = strings.TrimSpace(item.Title)
 		if entry.Title == "" {
+			// The entry title is optional, so we need to find a fallback.
 			for _, value := range []string{item.Summary, item.ContentText, item.ContentHTML} {
 				if value = sanitizer.TruncateHTML(value, 100); value == "" {
 					continue
@@ -99,29 +101,34 @@ func (j *JSONAdapter) BuildFeed(baseURL string) *model.Feed {
 
 		// Populate the entry content.
 		for _, value := range []string{item.ContentHTML, item.ContentText, item.Summary} {
-			value = strings.TrimSpace(value)
-			if value != "" {
-				entry.Content = value
-				break
+			if value = strings.TrimSpace(value); value == "" {
+				continue
 			}
+
+			entry.Content = value
+			break
 		}
 
 		// Populate the entry date.
 		for _, value := range []string{item.DatePublished, item.DateModified} {
-			value = strings.TrimSpace(value)
-			if value != "" {
-				if date, err := date.Parse(value); err != nil {
-					slog.Debug("Unable to parse date from JSON feed",
-						slog.String("date", value),
-						slog.String("url", entry.URL),
-						slog.Any("error", err),
-					)
-				} else {
-					entry.Date = date
-					break
-				}
+			if value = strings.TrimSpace(value); value == "" {
+				continue
+			}
+
+			parsedDate, err := date.Parse(value)
+			if err != nil {
+				slog.Debug("Unable to parse date from JSON feed",
+					slog.String("date", value),
+					slog.String("url", entry.URL),
+					slog.Any("error", err),
+				)
+				continue
 			}
+
+			entry.Date = parsedDate
+			break
 		}
+
 		if entry.Date.IsZero() {
 			entry.Date = time.Now()
 		}
@@ -146,15 +153,25 @@ func (j *JSONAdapter) BuildFeed(baseURL string) *model.Feed {
 		// Populate the entry enclosures.
 		for _, attachment := range item.Attachments {
 			attachmentURL := strings.TrimSpace(attachment.URL)
-			if attachmentURL != "" {
-				if absoluteAttachmentURL, err := urllib.ResolveToAbsoluteURL(feed.SiteURL, attachmentURL); err == nil {
-					entry.Enclosures = append(entry.Enclosures, &model.Enclosure{
-						URL:      absoluteAttachmentURL,
-						MimeType: attachment.MimeType,
-						Size:     attachment.Size,
-					})
-				}
+			if attachmentURL == "" {
+				continue
 			}
+
+			absoluteAttachmentURL, err := urllib.ResolveToAbsoluteURL(feed.SiteURL, attachmentURL)
+			if err != nil {
+				slog.Debug("Unable to build absolute URL for attachment",
+					slog.String("url", attachmentURL),
+					slog.String("site_url", feed.SiteURL),
+					slog.Any("error", err),
+				)
+				continue
+			}
+
+			entry.Enclosures = append(entry.Enclosures, &model.Enclosure{
+				URL:      absoluteAttachmentURL,
+				MimeType: attachment.MimeType,
+				Size:     attachment.Size,
+			})
 		}
 
 		// Populate the entry tags.

+ 106 - 85
internal/reader/rss/adapter.go

@@ -36,14 +36,16 @@ func (r *rssAdapter) buildFeed(baseURL string) *model.Feed {
 		feed.SiteURL = absoluteSiteURL
 	}
 
-	// Try to find the feed URL from the Atom links.
-	for _, atomLink := range r.rss.Channel.Links {
-		atomLinkHref := strings.TrimSpace(atomLink.Href)
-		if atomLinkHref != "" && atomLink.Rel == "self" {
-			if absoluteFeedURL, err := urllib.ResolveToAbsoluteURL(feed.FeedURL, atomLinkHref); err == nil {
-				feed.FeedURL = absoluteFeedURL
-				break
-			}
+	// Try to find the feed URL from the Channel links.
+	for _, link := range r.rss.Channel.Links {
+		href := strings.TrimSpace(link.Href)
+		if href == "" || link.Rel != "self" {
+			continue
+		}
+
+		if absoluteFeedURL, err := urllib.ResolveToAbsoluteURL(feed.FeedURL, href); err == nil {
+			feed.FeedURL = absoluteFeedURL
+			break
 		}
 	}
 
@@ -78,19 +80,20 @@ func (r *rssAdapter) buildFeed(baseURL string) *model.Feed {
 
 		// Populate the entry URL.
 		entryURL := findEntryURL(&item)
-		if entryURL == "" {
+		if entryURL != "" {
+			entry.URL = entryURL
+			if absoluteEntryURL, err := urllib.ResolveToAbsoluteURL(feed.SiteURL, entryURL); err == nil {
+				entry.URL = absoluteEntryURL
+			}
+		}
+
+		if entry.URL == "" {
+			// Fallback to the feed URL if no entry URL is found.
+			entry.URL = feed.SiteURL
+
 			// Fallback to the first enclosure URL if it exists.
 			if len(entry.Enclosures) > 0 && entry.Enclosures[0].URL != "" {
 				entry.URL = entry.Enclosures[0].URL
-			} else {
-				// Fallback to the feed URL if no entry URL is found.
-				entry.URL = feed.SiteURL
-			}
-		} else {
-			if absoluteEntryURL, err := urllib.ResolveToAbsoluteURL(feed.SiteURL, entryURL); err == nil {
-				entry.URL = absoluteEntryURL
-			} else {
-				entry.URL = entryURL
 			}
 		}
 
@@ -259,21 +262,21 @@ func findEntryDate(rssItem *rssItem) time.Time {
 		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()
-		}
+	if value = strings.TrimSpace(value); value == "" {
+		return time.Now()
+	}
 
-		return result
+	parsedDate, 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 time.Now()
+	return parsedDate
 }
 
 func findEntryAuthor(rssItem *rssItem) string {
@@ -333,22 +336,28 @@ func findEntryEnclosures(rssItem *rssItem, siteURL string) model.EnclosureList {
 		if mediaURL == "" {
 			continue
 		}
-		if _, found := duplicates[mediaURL]; !found {
-			if mediaAbsoluteURL, err := urllib.ResolveToAbsoluteURL(siteURL, mediaURL); err != nil {
-				slog.Debug("Unable to build absolute URL for media thumbnail",
-					slog.String("url", mediaThumbnail.URL),
-					slog.String("site_url", siteURL),
-					slog.Any("error", err),
-				)
-			} else {
-				duplicates[mediaAbsoluteURL] = true
-				enclosures = append(enclosures, &model.Enclosure{
-					URL:      mediaAbsoluteURL,
-					MimeType: mediaThumbnail.MimeType(),
-					Size:     mediaThumbnail.Size(),
-				})
-			}
+
+		mediaURL, err := urllib.ResolveToAbsoluteURL(siteURL, mediaURL)
+		if err != nil {
+			slog.Debug("Unable to build absolute URL for media thumbnail",
+				slog.String("url", mediaThumbnail.URL),
+				slog.String("site_url", siteURL),
+				slog.Any("error", err),
+			)
+			continue
 		}
+
+		if _, found := duplicates[mediaURL]; found {
+			continue
+		}
+
+		duplicates[mediaURL] = true
+
+		enclosures = append(enclosures, &model.Enclosure{
+			URL:      mediaURL,
+			MimeType: mediaThumbnail.MimeType(),
+			Size:     mediaThumbnail.Size(),
+		})
 	}
 
 	for _, enclosure := range rssItem.Enclosures {
@@ -370,15 +379,17 @@ func findEntryEnclosures(rssItem *rssItem, siteURL string) model.EnclosureList {
 			enclosureURL = absoluteEnclosureURL
 		}
 
-		if _, found := duplicates[enclosureURL]; !found {
-			duplicates[enclosureURL] = true
-
-			enclosures = append(enclosures, &model.Enclosure{
-				URL:      enclosureURL,
-				MimeType: enclosure.Type,
-				Size:     enclosure.Size(),
-			})
+		if _, found := duplicates[enclosureURL]; found {
+			continue
 		}
+
+		duplicates[enclosureURL] = true
+
+		enclosures = append(enclosures, &model.Enclosure{
+			URL:      enclosureURL,
+			MimeType: enclosure.Type,
+			Size:     enclosure.Size(),
+		})
 	}
 
 	for _, mediaContent := range mediaContents {
@@ -386,23 +397,28 @@ func findEntryEnclosures(rssItem *rssItem, siteURL string) model.EnclosureList {
 		if mediaURL == "" {
 			continue
 		}
-		if _, found := duplicates[mediaURL]; !found {
-			mediaURL := strings.TrimSpace(mediaContent.URL)
-			if mediaAbsoluteURL, err := urllib.ResolveToAbsoluteURL(siteURL, mediaURL); err != nil {
-				slog.Debug("Unable to build absolute URL for media content",
-					slog.String("url", mediaContent.URL),
-					slog.String("site_url", siteURL),
-					slog.Any("error", err),
-				)
-			} else {
-				duplicates[mediaAbsoluteURL] = true
-				enclosures = append(enclosures, &model.Enclosure{
-					URL:      mediaAbsoluteURL,
-					MimeType: mediaContent.MimeType(),
-					Size:     mediaContent.Size(),
-				})
-			}
+
+		mediaURL, err := urllib.ResolveToAbsoluteURL(siteURL, mediaURL)
+		if err != nil {
+			slog.Debug("Unable to build absolute URL for media content",
+				slog.String("url", mediaContent.URL),
+				slog.String("site_url", siteURL),
+				slog.Any("error", err),
+			)
+			continue
+		}
+
+		if _, found := duplicates[mediaURL]; found {
+			continue
 		}
+
+		duplicates[mediaURL] = true
+
+		enclosures = append(enclosures, &model.Enclosure{
+			URL:      mediaURL,
+			MimeType: mediaContent.MimeType(),
+			Size:     mediaContent.Size(),
+		})
 	}
 
 	for _, mediaPeerLink := range mediaPeerLinks {
@@ -410,23 +426,28 @@ func findEntryEnclosures(rssItem *rssItem, siteURL string) model.EnclosureList {
 		if mediaURL == "" {
 			continue
 		}
-		if _, found := duplicates[mediaURL]; !found {
-			mediaURL := strings.TrimSpace(mediaPeerLink.URL)
-			if mediaAbsoluteURL, err := urllib.ResolveToAbsoluteURL(siteURL, mediaURL); err != nil {
-				slog.Debug("Unable to build absolute URL for media peer link",
-					slog.String("url", mediaPeerLink.URL),
-					slog.String("site_url", siteURL),
-					slog.Any("error", err),
-				)
-			} else {
-				duplicates[mediaAbsoluteURL] = true
-				enclosures = append(enclosures, &model.Enclosure{
-					URL:      mediaAbsoluteURL,
-					MimeType: mediaPeerLink.MimeType(),
-					Size:     mediaPeerLink.Size(),
-				})
-			}
+
+		mediaURL, err := urllib.ResolveToAbsoluteURL(siteURL, mediaURL)
+		if err != nil {
+			slog.Debug("Unable to build absolute URL for media peer link",
+				slog.String("url", mediaPeerLink.URL),
+				slog.String("site_url", siteURL),
+				slog.Any("error", err),
+			)
+			continue
+		}
+
+		if _, found := duplicates[mediaURL]; found {
+			continue
 		}
+
+		duplicates[mediaURL] = true
+
+		enclosures = append(enclosures, &model.Enclosure{
+			URL:      mediaURL,
+			MimeType: mediaPeerLink.MimeType(),
+			Size:     mediaPeerLink.Size(),
+		})
 	}
 
 	return enclosures

+ 1 - 2
internal/reader/rss/parser_test.go

@@ -1687,7 +1687,7 @@ func TestParseEntryWithMediaContent(t *testing.T) {
 	if len(feed.Entries) != 1 {
 		t.Fatalf("Incorrect number of entries, got: %d", len(feed.Entries))
 	}
-	if len(feed.Entries[0].Enclosures) != 4 {
+	if len(feed.Entries[0].Enclosures) != 3 {
 		t.Fatalf("Incorrect number of enclosures, got: %d", len(feed.Entries[0].Enclosures))
 	}
 
@@ -1696,7 +1696,6 @@ func TestParseEntryWithMediaContent(t *testing.T) {
 		mimeType string
 		size     int64
 	}{
-		{"https://example.org/thumbnail.jpg", "image/*", 0},
 		{"https://example.org/thumbnail.jpg", "image/*", 0},
 		{"https://example.org/media1.jpg", "image/*", 0},
 		{"https://example.org/media2.jpg", "image/*", 0},