Take RSS TTL field into consideration to schedule next check date
This commit is contained in:
parent
ed35555d74
commit
5e6c054345
5 changed files with 105 additions and 11 deletions
|
@ -49,14 +49,18 @@ type Feed struct {
|
||||||
IgnoreHTTPCache bool `json:"ignore_http_cache"`
|
IgnoreHTTPCache bool `json:"ignore_http_cache"`
|
||||||
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
|
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
|
||||||
FetchViaProxy bool `json:"fetch_via_proxy"`
|
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"`
|
HideGlobally bool `json:"hide_globally"`
|
||||||
UnreadCount int `json:"-"`
|
|
||||||
ReadCount int `json:"-"`
|
|
||||||
AppriseServiceURLs string `json:"apprise_service_urls"`
|
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 {
|
type FeedCounters struct {
|
||||||
|
|
|
@ -5,6 +5,7 @@ package handler // import "miniflux.app/v2/internal/reader/handler"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"time"
|
||||||
|
|
||||||
"miniflux.app/v2/internal/config"
|
"miniflux.app/v2/internal/config"
|
||||||
"miniflux.app/v2/internal/errors"
|
"miniflux.app/v2/internal/errors"
|
||||||
|
@ -185,6 +186,28 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool
|
||||||
return parseErr
|
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
|
originalFeed.Entries = updatedFeed.Entries
|
||||||
processor.ProcessFeedEntries(store, originalFeed, user, forceRefresh)
|
processor.ProcessFeedEntries(store, originalFeed, user, forceRefresh)
|
||||||
|
|
||||||
|
|
|
@ -1500,3 +1500,51 @@ func TestParseEntryWithCategoryAndCDATA(t *testing.T) {
|
||||||
t.Errorf("Incorrect entry category, got %q instead of %q", result, expected)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,12 +4,14 @@
|
||||||
package rss // import "miniflux.app/v2/internal/reader/rss"
|
package rss // import "miniflux.app/v2/internal/reader/rss"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"errors"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrInvalidDurationFormat = errors.New("rss: invalid duration format")
|
||||||
|
|
||||||
// PodcastFeedElement represents iTunes and GooglePlay feed XML elements.
|
// PodcastFeedElement represents iTunes and GooglePlay feed XML elements.
|
||||||
// Specs:
|
// Specs:
|
||||||
// - https://github.com/simplepie/simplepie-ng/wiki/Spec:-iTunes-Podcast-RSS
|
// - https://github.com/simplepie/simplepie-ng/wiki/Spec:-iTunes-Podcast-RSS
|
||||||
|
@ -74,21 +76,19 @@ func (e *PodcastEntryElement) PodcastDescription() string {
|
||||||
return strings.TrimSpace(description)
|
return strings.TrimSpace(description)
|
||||||
}
|
}
|
||||||
|
|
||||||
var invalidDurationFormatErr = fmt.Errorf("rss: invalid duration format")
|
|
||||||
|
|
||||||
// normalizeDuration returns the duration tag value as a number of minutes
|
// normalizeDuration returns the duration tag value as a number of minutes
|
||||||
func normalizeDuration(rawDuration string) (int, error) {
|
func normalizeDuration(rawDuration string) (int, error) {
|
||||||
var sumSeconds int
|
var sumSeconds int
|
||||||
|
|
||||||
durationParts := strings.Split(rawDuration, ":")
|
durationParts := strings.Split(rawDuration, ":")
|
||||||
if len(durationParts) > 3 {
|
if len(durationParts) > 3 {
|
||||||
return 0, invalidDurationFormatErr
|
return 0, ErrInvalidDurationFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, durationPart := range durationParts {
|
for i, durationPart := range durationParts {
|
||||||
durationPartValue, err := strconv.Atoi(durationPart)
|
durationPartValue, err := strconv.Atoi(durationPart)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, invalidDurationFormatErr
|
return 0, ErrInvalidDurationFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
sumSeconds += int(math.Pow(60, float64(len(durationParts)-i-1))) * durationPartValue
|
sumSeconds += int(math.Pow(60, float64(len(durationParts)-i-1))) * durationPartValue
|
||||||
|
|
|
@ -33,10 +33,28 @@ type rssFeed struct {
|
||||||
PubDate string `xml:"channel>pubDate"`
|
PubDate string `xml:"channel>pubDate"`
|
||||||
ManagingEditor string `xml:"channel>managingEditor"`
|
ManagingEditor string `xml:"channel>managingEditor"`
|
||||||
Webmaster string `xml:"channel>webMaster"`
|
Webmaster string `xml:"channel>webMaster"`
|
||||||
|
TimeToLive rssTTL `xml:"channel>ttl"`
|
||||||
Items []rssItem `xml:"channel>item"`
|
Items []rssItem `xml:"channel>item"`
|
||||||
PodcastFeedElement
|
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 {
|
func (r *rssFeed) Transform(baseURL string) *model.Feed {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
@ -60,6 +78,7 @@ func (r *rssFeed) Transform(baseURL string) *model.Feed {
|
||||||
}
|
}
|
||||||
|
|
||||||
feed.IconURL = strings.TrimSpace(r.ImageURL)
|
feed.IconURL = strings.TrimSpace(r.ImageURL)
|
||||||
|
feed.TTL = r.TimeToLive.Value()
|
||||||
|
|
||||||
for _, item := range r.Items {
|
for _, item := range r.Items {
|
||||||
entry := item.Transform()
|
entry := item.Transform()
|
||||||
|
|
Loading…
Reference in a new issue