From 6bc4b35e383f515526d2cd9805d67ce4c43a87f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Tue, 12 Mar 2024 20:31:08 -0700 Subject: [PATCH] Refactor RDF parser to use an adapter Avoid tight coupling between `model.Feed` and the original XML RDF feed. --- internal/reader/dublincore/dublincore.go | 20 +- internal/reader/rdf/adapter.go | 115 +++++ internal/reader/rdf/parser.go | 6 +- internal/reader/rdf/parser_test.go | 586 ++++++++++++++--------- internal/reader/rdf/rdf.go | 125 +---- 5 files changed, 480 insertions(+), 372 deletions(-) create mode 100644 internal/reader/rdf/adapter.go diff --git a/internal/reader/dublincore/dublincore.go b/internal/reader/dublincore/dublincore.go index fd4b4911..18c1265d 100644 --- a/internal/reader/dublincore/dublincore.go +++ b/internal/reader/dublincore/dublincore.go @@ -3,29 +3,13 @@ package dublincore // import "miniflux.app/v2/internal/reader/dublincore" -import ( - "strings" - - "miniflux.app/v2/internal/reader/sanitizer" -) - -// DublinCoreFeedElement represents Dublin Core feed XML elements. -type DublinCoreFeedElement struct { - DublinCoreCreator string `xml:"http://purl.org/dc/elements/1.1/ channel>creator"` +type DublinCoreChannelElement struct { + DublinCoreCreator string `xml:"http://purl.org/dc/elements/1.1/ creator"` } -func (feed *DublinCoreFeedElement) GetSanitizedCreator() string { - return strings.TrimSpace(sanitizer.StripTags(feed.DublinCoreCreator)) -} - -// DublinCoreItemElement represents Dublin Core entry XML elements. type DublinCoreItemElement struct { DublinCoreTitle string `xml:"http://purl.org/dc/elements/1.1/ title"` DublinCoreDate string `xml:"http://purl.org/dc/elements/1.1/ date"` DublinCoreCreator string `xml:"http://purl.org/dc/elements/1.1/ creator"` DublinCoreContent string `xml:"http://purl.org/rss/1.0/modules/content/ encoded"` } - -func (item *DublinCoreItemElement) GetSanitizedCreator() string { - return strings.TrimSpace(sanitizer.StripTags(item.DublinCoreCreator)) -} diff --git a/internal/reader/rdf/adapter.go b/internal/reader/rdf/adapter.go new file mode 100644 index 00000000..812badbc --- /dev/null +++ b/internal/reader/rdf/adapter.go @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package rdf // import "miniflux.app/v2/internal/reader/rdf" + +import ( + "html" + "log/slog" + "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 RDFAdapter struct { + rdf *RDF +} + +func NewRDFAdapter(rdf *RDF) *RDFAdapter { + return &RDFAdapter{rdf} +} + +func (r *RDFAdapter) BuildFeed(feedURL string) *model.Feed { + feed := &model.Feed{ + Title: stripTags(r.rdf.Channel.Title), + FeedURL: feedURL, + } + + 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 { + feed.SiteURL = siteURL + } + + for _, item := range r.rdf.Items { + entry := model.NewEntry() + itemLink := strings.TrimSpace(item.Link) + + // Populate the entry URL. + if itemLink == "" { + entry.URL = feed.SiteURL // Fallback to the feed URL if the entry URL is empty. + } else if entryURL, err := urllib.AbsoluteURL(feed.SiteURL, itemLink); err == nil { + entry.URL = entryURL + } else { + entry.URL = itemLink + } + + // Populate the entry title. + for _, title := range []string{item.Title, item.DublinCoreTitle} { + title = strings.TrimSpace(title) + if title != "" { + entry.Title = html.UnescapeString(title) + break + } + } + + // If the entry title is empty, we use the entry URL as a fallback. + if entry.Title == "" { + entry.Title = entry.URL + } + + // Populate the entry content. + if item.DublinCoreContent != "" { + entry.Content = item.DublinCoreContent + } else { + entry.Content = item.Description + } + + // Generate the entry hash. + hashValue := itemLink + if hashValue == "" { + hashValue = item.Title + item.Description // Fallback to the title and description if the link is empty. + } + + entry.Hash = crypto.Hash(hashValue) + + // Populate the entry date. + entry.Date = time.Now() + if item.DublinCoreDate != "" { + if itemDate, err := date.Parse(item.DublinCoreDate); err != nil { + slog.Debug("Unable to parse date from RDF feed", + slog.String("date", item.DublinCoreDate), + slog.String("link", itemLink), + slog.Any("error", err), + ) + } else { + entry.Date = itemDate + } + } + + // Populate the entry author. + switch { + case item.DublinCoreCreator != "": + entry.Author = stripTags(item.DublinCoreCreator) + case r.rdf.Channel.DublinCoreCreator != "": + entry.Author = stripTags(r.rdf.Channel.DublinCoreCreator) + } + + feed.Entries = append(feed.Entries, entry) + } + + return feed +} + +func stripTags(value string) string { + return strings.TrimSpace(sanitizer.StripTags(value)) +} diff --git a/internal/reader/rdf/parser.go b/internal/reader/rdf/parser.go index 695fb5ce..f743c5d7 100644 --- a/internal/reader/rdf/parser.go +++ b/internal/reader/rdf/parser.go @@ -13,10 +13,10 @@ import ( // Parse returns a normalized feed struct from a RDF feed. func Parse(baseURL string, data io.ReadSeeker) (*model.Feed, error) { - feed := new(rdfFeed) - if err := xml.NewXMLDecoder(data).Decode(feed); err != nil { + xmlFeed := new(RDF) + if err := xml.NewXMLDecoder(data).Decode(xmlFeed); err != nil { return nil, fmt.Errorf("rdf: unable to parse feed: %w", err) } - return feed.Transform(baseURL), nil + return NewRDFAdapter(xmlFeed).BuildFeed(baseURL), nil } diff --git a/internal/reader/rdf/parser_test.go b/internal/reader/rdf/parser_test.go index 146c6c95..5009a412 100644 --- a/internal/reader/rdf/parser_test.go +++ b/internal/reader/rdf/parser_test.go @@ -228,63 +228,87 @@ func TestParseRDFSampleWithDublinCore(t *testing.T) { } } -func TestParseItemWithOnlyFeedAuthor(t *testing.T) { +func TestParseRDFFeedWithEmptyTitle(t *testing.T) { data := ` - - - - Meerkat - http://meerkat.oreillynet.com - Rael Dornfest (mailto:rael@oreilly.com) - - - - XML: A Disruptive Technology - http://c.moreover.com/click/here.pl?r123 - - XML is placing increasingly heavy loads on the existing technical - infrastructure of the Internet. - - + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://purl.org/rss/1.0/"> + + http://example.org/item + + + Example + http://example.org/item + Test + ` - feed, err := Parse("http://meerkat.oreillynet.com", bytes.NewReader([]byte(data))) + feed, err := Parse("http://example.org/feed", bytes.NewReader([]byte(data))) if err != nil { t.Fatal(err) } - if feed.Entries[0].Author != "Rael Dornfest (mailto:rael@oreilly.com)" { - t.Errorf("Incorrect entry author, got: %s", feed.Entries[0].Author) + if feed.Title != "http://example.org/feed" { + t.Errorf(`Incorrect title, got: %q`, feed.Title) } } -func TestParseItemRelativeURL(t *testing.T) { +func TestParseRDFFeedWithEmptyLink(t *testing.T) { data := ` - - + + + Example Feed + + Example - http://example.org - - - - Title + http://example.org/item Test - something.html - + ` - feed, err := Parse("http://meerkat.oreillynet.com", bytes.NewReader([]byte(data))) + feed, err := Parse("http://example.org/feed", bytes.NewReader([]byte(data))) if err != nil { t.Fatal(err) } - if feed.Entries[0].URL != "http://example.org/something.html" { - t.Errorf("Incorrect entry url, got: %s", feed.Entries[0].URL) + if feed.SiteURL != "http://example.org/feed" { + t.Errorf(`Incorrect SiteURL, got: %q`, feed.SiteURL) + } + + if feed.FeedURL != "http://example.org/feed" { + t.Errorf(`Incorrect FeedURL, got: %q`, feed.FeedURL) + } +} + +func TestParseRDFFeedWithRelativeLink(t *testing.T) { + data := ` + + + Example Feed + /test/index.html + + + Example + http://example.org/item + Test + + ` + + feed, err := Parse("http://example.org/feed", bytes.NewReader([]byte(data))) + if err != nil { + t.Fatal(err) + } + + if feed.SiteURL != "http://example.org/test/index.html" { + t.Errorf(`Incorrect SiteURL, got: %q`, feed.SiteURL) + } + + if feed.FeedURL != "http://example.org/feed" { + t.Errorf(`Incorrect FeedURL, got: %q`, feed.FeedURL) } } @@ -321,63 +345,7 @@ func TestParseItemWithoutLink(t *testing.T) { } } -func TestParseItemWithDublicCoreDate(t *testing.T) { - data := ` - - - Example - http://example.org - - - - Title - Test - http://example.org/test.html - Tester - 2018-04-10T05:00:00+00:00 - - ` - - feed, err := Parse("http://example.org", bytes.NewReader([]byte(data))) - if err != nil { - t.Fatal(err) - } - - expectedDate := time.Date(2018, time.April, 10, 5, 0, 0, 0, time.UTC) - if !feed.Entries[0].Date.Equal(expectedDate) { - t.Errorf("Incorrect entry date, got: %v, want: %v", feed.Entries[0].Date, expectedDate) - } -} - -func TestParseItemWithEncodedHTMLInDCCreatorField(t *testing.T) { - data := ` - - - Example - http://example.org - - - - Title - Test - http://example.org/test.html - <a href="http://example.org/author1">Author 1</a> (University 1), <a href="http://example.org/author2">Author 2</a> (University 2) - 2018-04-10T05:00:00+00:00 - - ` - - feed, err := Parse("http://example.org", bytes.NewReader([]byte(data))) - if err != nil { - t.Fatal(err) - } - - expectedAuthor := "Author 1 (University 1), Author 2 (University 2)" - if feed.Entries[0].Author != expectedAuthor { - t.Errorf("Incorrect entry author, got: %s, want: %s", feed.Entries[0].Author, expectedAuthor) - } -} - -func TestParseItemWithoutDate(t *testing.T) { +func TestParseItemRelativeURL(t *testing.T) { data := ` @@ -388,90 +356,17 @@ func TestParseItemWithoutDate(t *testing.T) { Title Test - http://example.org/test.html + something.html ` - feed, err := Parse("http://example.org", bytes.NewReader([]byte(data))) + feed, err := Parse("http://meerkat.oreillynet.com", bytes.NewReader([]byte(data))) if err != nil { t.Fatal(err) } - expectedDate := time.Now().In(time.Local) - diff := expectedDate.Sub(feed.Entries[0].Date) - if diff > time.Second { - t.Errorf("Incorrect entry date, got: %v", diff) - } -} - -func TestParseItemWithEncodedHTMLTitle(t *testing.T) { - data := ` - - - Example - http://example.org - - - - AT&amp;T - Test - http://example.org/test.html - - ` - - feed, err := Parse("http://example.org", bytes.NewReader([]byte(data))) - if err != nil { - t.Fatal(err) - } - - if feed.Entries[0].Title != `AT&T` { - t.Errorf("Incorrect entry title, got: %q", feed.Entries[0].Title) - } -} - -func TestParseInvalidXml(t *testing.T) { - data := `garbage` - _, err := Parse("http://example.org", bytes.NewReader([]byte(data))) - if err == nil { - t.Fatal("Parse should returns an error") - } -} - -func TestParseFeedWithHTMLEntity(t *testing.T) { - data := ` - - - Example   Feed - http://example.org - - ` - - feed, err := Parse("http://example.org", bytes.NewReader([]byte(data))) - if err != nil { - t.Fatal(err) - } - - if feed.Title != "Example \u00a0 Feed" { - t.Errorf(`Incorrect title, got: %q`, feed.Title) - } -} - -func TestParseFeedWithInvalidCharacterEntity(t *testing.T) { - data := ` - - - Example Feed - http://example.org/a&b - - ` - - feed, err := Parse("http://example.org", bytes.NewReader([]byte(data))) - if err != nil { - t.Fatal(err) - } - - if feed.SiteURL != "http://example.org/a&b" { - t.Errorf(`Incorrect URL, got: %q`, feed.SiteURL) + if feed.Entries[0].URL != "http://example.org/something.html" { + t.Errorf("Incorrect entry url, got: %s", feed.Entries[0].URL) } } @@ -539,6 +434,130 @@ func TestParseFeedWithURLWrappedInSpaces(t *testing.T) { } } +func TestParseRDFItemWitEmptyTitleElement(t *testing.T) { + data := ` + + + Example Feed + http://example.org/ + + + + http://example.org/item + Test + + ` + + feed, err := Parse("http://example.org/", bytes.NewReader([]byte(data))) + if err != nil { + t.Fatal(err) + } + + if len(feed.Entries) != 1 { + t.Fatalf(`Unexpected number of entries, got %d`, len(feed.Entries)) + } + + expected := `http://example.org/item` + result := feed.Entries[0].Title + if result != expected { + t.Errorf(`Unexpected entry title, got %q instead of %q`, result, expected) + } +} + +func TestParseRDFItemWithDublinCoreTitleElement(t *testing.T) { + data := ` + + + Example Feed + http://example.org/ + + + Dublin Core Title + http://example.org/ + Test + + ` + + feed, err := Parse("http://example.org/", bytes.NewReader([]byte(data))) + if err != nil { + t.Fatal(err) + } + + if len(feed.Entries) != 1 { + t.Fatalf(`Unexpected number of entries, got %d`, len(feed.Entries)) + } + + expected := `Dublin Core Title` + result := feed.Entries[0].Title + if result != expected { + t.Errorf(`Unexpected entry title, got %q instead of %q`, result, expected) + } +} + +func TestParseRDFItemWithDuplicateTitleElement(t *testing.T) { + data := ` + + + Example Feed + http://example.org/ + + + Item Title + + http://example.org/ + Test + + ` + + feed, err := Parse("http://example.org/", bytes.NewReader([]byte(data))) + if err != nil { + t.Fatal(err) + } + + if len(feed.Entries) != 1 { + t.Fatalf(`Unexpected number of entries, got %d`, len(feed.Entries)) + } + + expected := `Item Title` + result := feed.Entries[0].Title + if result != expected { + t.Errorf(`Unexpected entry title, got %q instead of %q`, result, expected) + } +} + +func TestParseItemWithEncodedHTMLTitle(t *testing.T) { + data := ` + + + Example + http://example.org + + + + AT&amp;T + Test + http://example.org/test.html + + ` + + feed, err := Parse("http://example.org", bytes.NewReader([]byte(data))) + if err != nil { + t.Fatal(err) + } + + if feed.Entries[0].Title != `AT&T` { + t.Errorf("Incorrect entry title, got: %q", feed.Entries[0].Title) + } +} + func TestParseRDFWithContentEncoded(t *testing.T) { data := ` - - - Example Feed - http://example.org/ - - - Item Title - - http://example.org/ + + + Example + http://example.org + + + + Title Test - + http://example.org/test.html + ` - feed, err := Parse("http://example.org/", bytes.NewReader([]byte(data))) + feed, err := Parse("http://example.org", bytes.NewReader([]byte(data))) if err != nil { t.Fatal(err) } - if len(feed.Entries) != 1 { - t.Fatalf(`Unexpected number of entries, got %d`, len(feed.Entries)) - } - - expected := `Item Title` - result := feed.Entries[0].Title - if result != expected { - t.Errorf(`Unexpected entry title, got %q instead of %q`, result, expected) + expectedDate := time.Now().In(time.Local) + diff := expectedDate.Sub(feed.Entries[0].Date) + if diff > time.Second { + t.Errorf("Incorrect entry date, got: %v", diff) } } -func TestParseRDFItemWithDublinCoreTitleElement(t *testing.T) { +func TestParseItemWithDublicCoreDate(t *testing.T) { data := ` - - - Example Feed - http://example.org/ - - - Dublin Core Title - http://example.org/ + + + Example + http://example.org + + + + Title Test - + http://example.org/test.html + Tester + 2018-04-10T05:00:00+00:00 + ` - feed, err := Parse("http://example.org/", bytes.NewReader([]byte(data))) + feed, err := Parse("http://example.org", bytes.NewReader([]byte(data))) if err != nil { t.Fatal(err) } - if len(feed.Entries) != 1 { - t.Fatalf(`Unexpected number of entries, got %d`, len(feed.Entries)) - } - - expected := `Dublin Core Title` - result := feed.Entries[0].Title - if result != expected { - t.Errorf(`Unexpected entry title, got %q instead of %q`, result, expected) + expectedDate := time.Date(2018, time.April, 10, 5, 0, 0, 0, time.UTC) + if !feed.Entries[0].Date.Equal(expectedDate) { + t.Errorf("Incorrect entry date, got: %v, want: %v", feed.Entries[0].Date, expectedDate) } } -func TestParseRDFItemWitEmptyTitleElement(t *testing.T) { +func TestParseItemWithInvalidDublicCoreDate(t *testing.T) { data := ` - - - Example Feed - http://example.org/ - - - - http://example.org/item + + + Example + http://example.org + + + + Title Test - + http://example.org/test.html + Tester + 20-04-10T05:00:00+00:00 + ` - feed, err := Parse("http://example.org/", bytes.NewReader([]byte(data))) + feed, err := Parse("http://example.org", bytes.NewReader([]byte(data))) if err != nil { t.Fatal(err) } - if len(feed.Entries) != 1 { - t.Fatalf(`Unexpected number of entries, got %d`, len(feed.Entries)) - } - - expected := `http://example.org/item` - result := feed.Entries[0].Title - if result != expected { - t.Errorf(`Unexpected entry title, got %q instead of %q`, result, expected) + expectedDate := time.Now().In(time.Local) + diff := expectedDate.Sub(feed.Entries[0].Date) + if diff > time.Second { + t.Errorf("Incorrect entry date, got: %v", diff) + } +} + +func TestParseItemWithEncodedHTMLInDCCreatorField(t *testing.T) { + data := ` + + + Example + http://example.org + + + + Title + Test + http://example.org/test.html + <a href="http://example.org/author1">Author 1</a> (University 1), <a href="http://example.org/author2">Author 2</a> (University 2) + 2018-04-10T05:00:00+00:00 + + ` + + feed, err := Parse("http://example.org", bytes.NewReader([]byte(data))) + if err != nil { + t.Fatal(err) + } + + expectedAuthor := "Author 1 (University 1), Author 2 (University 2)" + if feed.Entries[0].Author != expectedAuthor { + t.Errorf("Incorrect entry author, got: %s, want: %s", feed.Entries[0].Author, expectedAuthor) + } +} + +func TestParseItemWithOnlyFeedAuthor(t *testing.T) { + data := ` + + + + Meerkat + http://meerkat.oreillynet.com + Rael Dornfest (mailto:rael@oreilly.com) + + + + XML: A Disruptive Technology + http://c.moreover.com/click/here.pl?r123 + + XML is placing increasingly heavy loads on the existing technical + infrastructure of the Internet. + + + ` + + feed, err := Parse("http://meerkat.oreillynet.com", bytes.NewReader([]byte(data))) + if err != nil { + t.Fatal(err) + } + + if feed.Entries[0].Author != "Rael Dornfest (mailto:rael@oreilly.com)" { + t.Errorf("Incorrect entry author, got: %s", feed.Entries[0].Author) + } +} + +func TestParseInvalidXml(t *testing.T) { + data := `garbage` + _, err := Parse("http://example.org", bytes.NewReader([]byte(data))) + if err == nil { + t.Fatal("Parse should returns an error") + } +} + +func TestParseFeedWithHTMLEntity(t *testing.T) { + data := ` + + + Example   Feed + http://example.org + + ` + + feed, err := Parse("http://example.org", bytes.NewReader([]byte(data))) + if err != nil { + t.Fatal(err) + } + + if feed.Title != "Example \u00a0 Feed" { + t.Errorf(`Incorrect title, got: %q`, feed.Title) + } +} + +func TestParseFeedWithInvalidCharacterEntity(t *testing.T) { + data := ` + + + Example Feed + http://example.org/a&b + + ` + + feed, err := Parse("http://example.org", bytes.NewReader([]byte(data))) + if err != nil { + t.Fatal(err) + } + + if feed.SiteURL != "http://example.org/a&b" { + t.Errorf(`Incorrect URL, got: %q`, feed.SiteURL) } } diff --git a/internal/reader/rdf/rdf.go b/internal/reader/rdf/rdf.go index 8ce454d7..5adaeeb9 100644 --- a/internal/reader/rdf/rdf.go +++ b/internal/reader/rdf/rdf.go @@ -5,130 +5,27 @@ package rdf // import "miniflux.app/v2/internal/reader/rdf" import ( "encoding/xml" - "html" - "log/slog" - "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/sanitizer" - "miniflux.app/v2/internal/urllib" ) -type rdfFeed struct { - XMLName xml.Name `xml:"RDF"` - Title string `xml:"channel>title"` - Link string `xml:"channel>link"` - Items []rdfItem `xml:"item"` - dublincore.DublinCoreFeedElement +// RDF sepcs: https://web.resource.org/rss/1.0/spec +type RDF struct { + XMLName xml.Name `xml:"http://www.w3.org/1999/02/22-rdf-syntax-ns# RDF"` + Channel RDFChannel `xml:"channel"` + Items []RDFItem `xml:"item"` } -func (r *rdfFeed) Transform(baseURL string) *model.Feed { - var err error - feed := new(model.Feed) - feed.Title = sanitizer.StripTags(r.Title) - feed.FeedURL = baseURL - feed.SiteURL, err = urllib.AbsoluteURL(baseURL, r.Link) - if err != nil { - feed.SiteURL = r.Link - } - - for _, item := range r.Items { - entry := item.Transform() - if entry.Author == "" && r.DublinCoreCreator != "" { - entry.Author = r.GetSanitizedCreator() - } - - if entry.URL == "" { - entry.URL = feed.SiteURL - } else { - entryURL, err := urllib.AbsoluteURL(feed.SiteURL, entry.URL) - if err == nil { - entry.URL = entryURL - } - } - - feed.Entries = append(feed.Entries, entry) - } - - return feed +type RDFChannel struct { + Title string `xml:"title"` + Link string `xml:"link"` + Description string `xml:"description"` + dublincore.DublinCoreChannelElement } -type rdfItem struct { +type RDFItem struct { Title string `xml:"http://purl.org/rss/1.0/ title"` Link string `xml:"link"` Description string `xml:"description"` dublincore.DublinCoreItemElement } - -func (r *rdfItem) Transform() *model.Entry { - entry := model.NewEntry() - entry.Title = r.entryTitle() - entry.Author = r.entryAuthor() - entry.URL = r.entryURL() - entry.Content = r.entryContent() - entry.Hash = r.entryHash() - entry.Date = r.entryDate() - - if entry.Title == "" { - entry.Title = entry.URL - } - return entry -} - -func (r *rdfItem) entryTitle() string { - for _, title := range []string{r.Title, r.DublinCoreTitle} { - title = strings.TrimSpace(title) - if title != "" { - return html.UnescapeString(title) - } - } - return "" -} - -func (r *rdfItem) entryContent() string { - switch { - case r.DublinCoreContent != "": - return r.DublinCoreContent - default: - return r.Description - } -} - -func (r *rdfItem) entryAuthor() string { - return r.GetSanitizedCreator() -} - -func (r *rdfItem) entryURL() string { - return strings.TrimSpace(r.Link) -} - -func (r *rdfItem) entryDate() time.Time { - if r.DublinCoreDate != "" { - result, err := date.Parse(r.DublinCoreDate) - if err != nil { - slog.Debug("Unable to parse date from RDF feed", - slog.String("date", r.DublinCoreDate), - slog.String("link", r.Link), - slog.Any("error", err), - ) - return time.Now() - } - - return result - } - - return time.Now() -} - -func (r *rdfItem) entryHash() string { - value := r.Link - if value == "" { - value = r.Title + r.Description - } - - return crypto.Hash(value) -}