瀏覽代碼

Take RSS TTL field into consideration to schedule next check date

Frédéric Guillot 2 年之前
父節點
當前提交
5e6c054345

+ 10 - 6
internal/model/feed.go

@@ -49,14 +49,18 @@ type Feed struct {
 	IgnoreHTTPCache             bool      `json:"ignore_http_cache"`
 	AllowSelfSignedCertificates bool      `json:"allow_self_signed_certificates"`
 	FetchViaProxy               bool      `json:"fetch_via_proxy"`
-	Category                    *Category `json:"category,omitempty"`
-	Entries                     Entries   `json:"entries,omitempty"`
-	IconURL                     string    `json:"-"`
-	Icon                        *FeedIcon `json:"icon"`
 	HideGlobally                bool      `json:"hide_globally"`
-	UnreadCount                 int       `json:"-"`
-	ReadCount                   int       `json:"-"`
 	AppriseServiceURLs          string    `json:"apprise_service_urls"`
+
+	// Non persisted attributes
+	Category *Category `json:"category,omitempty"`
+	Icon     *FeedIcon `json:"icon"`
+	Entries  Entries   `json:"entries,omitempty"`
+
+	TTL         int    `json:"-"`
+	IconURL     string `json:"-"`
+	UnreadCount int    `json:"-"`
+	ReadCount   int    `json:"-"`
 }
 
 type FeedCounters struct {

+ 23 - 0
internal/reader/handler/handler.go

@@ -5,6 +5,7 @@ package handler // import "miniflux.app/v2/internal/reader/handler"
 
 import (
 	"log/slog"
+	"time"
 
 	"miniflux.app/v2/internal/config"
 	"miniflux.app/v2/internal/errors"
@@ -185,6 +186,28 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool
 			return parseErr
 		}
 
+		// If the feed has a TTL defined, we use it to make sure we don't check it too often.
+		if updatedFeed.TTL > 0 {
+			minNextCheckAt := time.Now().Add(time.Minute * time.Duration(updatedFeed.TTL))
+			slog.Debug("Feed TTL",
+				slog.Int64("user_id", userID),
+				slog.Int64("feed_id", feedID),
+				slog.Int("ttl", updatedFeed.TTL),
+				slog.Time("next_check_at", originalFeed.NextCheckAt),
+			)
+
+			if originalFeed.NextCheckAt.IsZero() || originalFeed.NextCheckAt.Before(minNextCheckAt) {
+				slog.Debug("Updating next check date based on TTL",
+					slog.Int64("user_id", userID),
+					slog.Int64("feed_id", feedID),
+					slog.Int("ttl", updatedFeed.TTL),
+					slog.Time("new_next_check_at", minNextCheckAt),
+					slog.Time("old_next_check_at", originalFeed.NextCheckAt),
+				)
+				originalFeed.NextCheckAt = minNextCheckAt
+			}
+		}
+
 		originalFeed.Entries = updatedFeed.Entries
 		processor.ProcessFeedEntries(store, originalFeed, user, forceRefresh)
 

+ 48 - 0
internal/reader/rss/parser_test.go

@@ -1500,3 +1500,51 @@ func TestParseEntryWithCategoryAndCDATA(t *testing.T) {
 		t.Errorf("Incorrect entry category, got %q instead of %q", result, expected)
 	}
 }
+
+func TestParseFeedWithTTLField(t *testing.T) {
+	data := `<?xml version="1.0" encoding="utf-8"?>
+		<rss version="2.0">
+		<channel>
+			<title>Example</title>
+			<link>https://example.org/</link>
+			<ttl>60</ttl>
+			<item>
+				<title>Test</title>
+				<link>https://example.org/item</link>
+			</item>
+		</channel>
+		</rss>`
+
+	feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if feed.TTL != 60 {
+		t.Errorf("Incorrect TTL, got: %d", feed.TTL)
+	}
+}
+
+func TestParseFeedWithIncorrectTTLValue(t *testing.T) {
+	data := `<?xml version="1.0" encoding="utf-8"?>
+		<rss version="2.0">
+		<channel>
+			<title>Example</title>
+			<link>https://example.org/</link>
+			<ttl>invalid</ttl>
+			<item>
+				<title>Test</title>
+				<link>https://example.org/item</link>
+			</item>
+		</channel>
+		</rss>`
+
+	feed, err := Parse("https://example.org/", bytes.NewBufferString(data))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if feed.TTL != 0 {
+		t.Errorf("Incorrect TTL, got: %d", feed.TTL)
+	}
+}

+ 5 - 5
internal/reader/rss/podcast.go

@@ -4,12 +4,14 @@
 package rss // import "miniflux.app/v2/internal/reader/rss"
 
 import (
-	"fmt"
+	"errors"
 	"math"
 	"strconv"
 	"strings"
 )
 
+var ErrInvalidDurationFormat = errors.New("rss: invalid duration format")
+
 // PodcastFeedElement represents iTunes and GooglePlay feed XML elements.
 // Specs:
 // - https://github.com/simplepie/simplepie-ng/wiki/Spec:-iTunes-Podcast-RSS
@@ -74,21 +76,19 @@ func (e *PodcastEntryElement) PodcastDescription() string {
 	return strings.TrimSpace(description)
 }
 
-var invalidDurationFormatErr = fmt.Errorf("rss: invalid duration format")
-
 // normalizeDuration returns the duration tag value as a number of minutes
 func normalizeDuration(rawDuration string) (int, error) {
 	var sumSeconds int
 
 	durationParts := strings.Split(rawDuration, ":")
 	if len(durationParts) > 3 {
-		return 0, invalidDurationFormatErr
+		return 0, ErrInvalidDurationFormat
 	}
 
 	for i, durationPart := range durationParts {
 		durationPartValue, err := strconv.Atoi(durationPart)
 		if err != nil {
-			return 0, invalidDurationFormatErr
+			return 0, ErrInvalidDurationFormat
 		}
 
 		sumSeconds += int(math.Pow(60, float64(len(durationParts)-i-1))) * durationPartValue

+ 19 - 0
internal/reader/rss/rss.go

@@ -33,10 +33,28 @@ type rssFeed struct {
 	PubDate        string    `xml:"channel>pubDate"`
 	ManagingEditor string    `xml:"channel>managingEditor"`
 	Webmaster      string    `xml:"channel>webMaster"`
+	TimeToLive     rssTTL    `xml:"channel>ttl"`
 	Items          []rssItem `xml:"channel>item"`
 	PodcastFeedElement
 }
 
+type rssTTL struct {
+	Data string `xml:",chardata"`
+}
+
+func (r *rssTTL) Value() int {
+	if r.Data == "" {
+		return 0
+	}
+
+	value, err := strconv.Atoi(r.Data)
+	if err != nil {
+		return 0
+	}
+
+	return value
+}
+
 func (r *rssFeed) Transform(baseURL string) *model.Feed {
 	var err error
 
@@ -60,6 +78,7 @@ func (r *rssFeed) Transform(baseURL string) *model.Feed {
 	}
 
 	feed.IconURL = strings.TrimSpace(r.ImageURL)
+	feed.TTL = r.TimeToLive.Value()
 
 	for _, item := range r.Items {
 		entry := item.Transform()