mirror of
https://github.com/miniflux/v2.git
synced 2025-06-27 16:36:00 +00:00
feat: update feed icon during force refresh
This commit is contained in:
parent
562a7b79a5
commit
f16735fd6d
5 changed files with 139 additions and 99 deletions
|
@ -13,7 +13,7 @@ import (
|
||||||
func (h *handler) getIconByFeedID(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) getIconByFeedID(w http.ResponseWriter, r *http.Request) {
|
||||||
feedID := request.RouteInt64Param(r, "feedID")
|
feedID := request.RouteInt64Param(r, "feedID")
|
||||||
|
|
||||||
if !h.store.HasIcon(feedID) {
|
if !h.store.HasFeedIcon(feedID) {
|
||||||
json.NotFound(w, r)
|
json.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,11 +56,12 @@ type Feed struct {
|
||||||
NtfyEnabled bool `json:"ntfy_enabled"`
|
NtfyEnabled bool `json:"ntfy_enabled"`
|
||||||
NtfyPriority int `json:"ntfy_priority"`
|
NtfyPriority int `json:"ntfy_priority"`
|
||||||
|
|
||||||
// Non persisted attributes
|
// Non-persisted attributes
|
||||||
Category *Category `json:"category,omitempty"`
|
Category *Category `json:"category,omitempty"`
|
||||||
Icon *FeedIcon `json:"icon"`
|
Icon *FeedIcon `json:"icon"`
|
||||||
Entries Entries `json:"entries,omitempty"`
|
Entries Entries `json:"entries,omitempty"`
|
||||||
|
|
||||||
|
// Internal attributes (not exposed in the API and not persisted in the database)
|
||||||
TTL int `json:"-"`
|
TTL int `json:"-"`
|
||||||
IconURL string `json:"-"`
|
IconURL string `json:"-"`
|
||||||
UnreadCount int `json:"-"`
|
UnreadCount int `json:"-"`
|
||||||
|
|
|
@ -93,13 +93,7 @@ func CreateFeedFromSubscriptionDiscovery(store *storage.Storage, userID int64, f
|
||||||
requestBuilder.IgnoreTLSErrors(feedCreationRequest.AllowSelfSignedCertificates)
|
requestBuilder.IgnoreTLSErrors(feedCreationRequest.AllowSelfSignedCertificates)
|
||||||
requestBuilder.DisableHTTP2(feedCreationRequest.DisableHTTP2)
|
requestBuilder.DisableHTTP2(feedCreationRequest.DisableHTTP2)
|
||||||
|
|
||||||
checkFeedIcon(
|
icon.NewIconChecker(store, subscription).UpdateOrCreateFeedIcon()
|
||||||
store,
|
|
||||||
requestBuilder,
|
|
||||||
subscription.ID,
|
|
||||||
subscription.SiteURL,
|
|
||||||
subscription.IconURL,
|
|
||||||
)
|
|
||||||
|
|
||||||
return subscription, nil
|
return subscription, nil
|
||||||
}
|
}
|
||||||
|
@ -188,13 +182,8 @@ func CreateFeed(store *storage.Storage, userID int64, feedCreationRequest *model
|
||||||
slog.String("feed_url", subscription.FeedURL),
|
slog.String("feed_url", subscription.FeedURL),
|
||||||
)
|
)
|
||||||
|
|
||||||
checkFeedIcon(
|
icon.NewIconChecker(store, subscription).UpdateOrCreateFeedIcon()
|
||||||
store,
|
|
||||||
requestBuilder,
|
|
||||||
subscription.ID,
|
|
||||||
subscription.SiteURL,
|
|
||||||
subscription.IconURL,
|
|
||||||
)
|
|
||||||
return subscription, nil
|
return subscription, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,6 +259,8 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool
|
||||||
slog.Debug("Feed modified",
|
slog.Debug("Feed modified",
|
||||||
slog.Int64("user_id", userID),
|
slog.Int64("user_id", userID),
|
||||||
slog.Int64("feed_id", feedID),
|
slog.Int64("feed_id", feedID),
|
||||||
|
slog.String("etag_header", originalFeed.EtagHeader),
|
||||||
|
slog.String("last_modified_header", originalFeed.LastModifiedHeader),
|
||||||
)
|
)
|
||||||
|
|
||||||
responseBody, localizedError := responseHandler.ReadBody(config.Opts.HTTPClientMaxBodySize())
|
responseBody, localizedError := responseHandler.ReadBody(config.Opts.HTTPClientMaxBodySize())
|
||||||
|
@ -293,8 +284,10 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool
|
||||||
|
|
||||||
// If the feed has a TTL defined, we use it to make sure we don't check it too often.
|
// If the feed has a TTL defined, we use it to make sure we don't check it too often.
|
||||||
newTTL = updatedFeed.TTL
|
newTTL = updatedFeed.TTL
|
||||||
|
|
||||||
// Set the next check at with updated arguments.
|
// Set the next check at with updated arguments.
|
||||||
originalFeed.ScheduleNextCheck(weeklyEntryCount, newTTL)
|
originalFeed.ScheduleNextCheck(weeklyEntryCount, newTTL)
|
||||||
|
|
||||||
slog.Debug("Updated next check date",
|
slog.Debug("Updated next check date",
|
||||||
slog.Int64("user_id", userID),
|
slog.Int64("user_id", userID),
|
||||||
slog.Int64("feed_id", feedID),
|
slog.Int64("feed_id", feedID),
|
||||||
|
@ -329,18 +322,18 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool
|
||||||
originalFeed.EtagHeader = responseHandler.ETag()
|
originalFeed.EtagHeader = responseHandler.ETag()
|
||||||
originalFeed.LastModifiedHeader = responseHandler.LastModified()
|
originalFeed.LastModifiedHeader = responseHandler.LastModified()
|
||||||
|
|
||||||
checkFeedIcon(
|
iconChecker := icon.NewIconChecker(store, originalFeed)
|
||||||
store,
|
if forceRefresh {
|
||||||
requestBuilder,
|
iconChecker.UpdateOrCreateFeedIcon()
|
||||||
originalFeed.ID,
|
} else {
|
||||||
originalFeed.SiteURL,
|
iconChecker.CreateFeedIconIfMissing()
|
||||||
updatedFeed.IconURL,
|
}
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
slog.Debug("Feed not modified",
|
slog.Debug("Feed not modified",
|
||||||
slog.Int64("user_id", userID),
|
slog.Int64("user_id", userID),
|
||||||
slog.Int64("feed_id", feedID),
|
slog.Int64("feed_id", feedID),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Last-Modified may be updated even if ETag is not. In this case, per
|
// Last-Modified may be updated even if ETag is not. In this case, per
|
||||||
// RFC9111 sections 3.2 and 4.3.4, the stored response must be updated.
|
// RFC9111 sections 3.2 and 4.3.4, the stored response must be updated.
|
||||||
if responseHandler.LastModified() != "" {
|
if responseHandler.LastModified() != "" {
|
||||||
|
@ -359,32 +352,3 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkFeedIcon(store *storage.Storage, requestBuilder *fetcher.RequestBuilder, feedID int64, websiteURL, feedIconURL string) {
|
|
||||||
if !store.HasIcon(feedID) {
|
|
||||||
iconFinder := icon.NewIconFinder(requestBuilder, websiteURL, feedIconURL)
|
|
||||||
if icon, err := iconFinder.FindIcon(); err != nil {
|
|
||||||
slog.Debug("Unable to find feed icon",
|
|
||||||
slog.Int64("feed_id", feedID),
|
|
||||||
slog.String("website_url", websiteURL),
|
|
||||||
slog.String("feed_icon_url", feedIconURL),
|
|
||||||
slog.Any("error", err),
|
|
||||||
)
|
|
||||||
} else if icon == nil {
|
|
||||||
slog.Debug("No icon found",
|
|
||||||
slog.Int64("feed_id", feedID),
|
|
||||||
slog.String("website_url", websiteURL),
|
|
||||||
slog.String("feed_icon_url", feedIconURL),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
if err := store.CreateFeedIcon(feedID, icon); err != nil {
|
|
||||||
slog.Error("Unable to store feed icon",
|
|
||||||
slog.Int64("feed_id", feedID),
|
|
||||||
slog.String("website_url", websiteURL),
|
|
||||||
slog.String("feed_icon_url", feedIconURL),
|
|
||||||
slog.Any("error", err),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
84
internal/reader/icon/checker.go
Normal file
84
internal/reader/icon/checker.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package icon // import "miniflux.app/v2/internal/reader/icon"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"miniflux.app/v2/internal/config"
|
||||||
|
"miniflux.app/v2/internal/model"
|
||||||
|
"miniflux.app/v2/internal/reader/fetcher"
|
||||||
|
"miniflux.app/v2/internal/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IconChecker struct {
|
||||||
|
store *storage.Storage
|
||||||
|
feed *model.Feed
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIconChecker(store *storage.Storage, feed *model.Feed) *IconChecker {
|
||||||
|
return &IconChecker{
|
||||||
|
store: store,
|
||||||
|
feed: feed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *IconChecker) fetchAndStoreIcon() {
|
||||||
|
requestBuilder := fetcher.NewRequestBuilder()
|
||||||
|
requestBuilder.WithUserAgent(c.feed.UserAgent, config.Opts.HTTPClientUserAgent())
|
||||||
|
requestBuilder.WithCookie(c.feed.Cookie)
|
||||||
|
requestBuilder.WithTimeout(config.Opts.HTTPClientTimeout())
|
||||||
|
requestBuilder.WithProxy(config.Opts.HTTPClientProxy())
|
||||||
|
requestBuilder.UseProxy(c.feed.FetchViaProxy)
|
||||||
|
requestBuilder.IgnoreTLSErrors(c.feed.AllowSelfSignedCertificates)
|
||||||
|
requestBuilder.DisableHTTP2(c.feed.DisableHTTP2)
|
||||||
|
|
||||||
|
iconFinder := NewIconFinder(requestBuilder, c.feed.FeedURL, c.feed.IconURL)
|
||||||
|
if icon, err := iconFinder.FindIcon(); err != nil {
|
||||||
|
slog.Debug("Unable to find feed icon",
|
||||||
|
slog.Int64("feed_id", c.feed.ID),
|
||||||
|
slog.String("website_url", c.feed.FeedURL),
|
||||||
|
slog.String("feed_icon_url", c.feed.IconURL),
|
||||||
|
slog.Any("error", err),
|
||||||
|
)
|
||||||
|
} else if icon == nil {
|
||||||
|
slog.Debug("No icon found",
|
||||||
|
slog.Int64("feed_id", c.feed.ID),
|
||||||
|
slog.String("website_url", c.feed.FeedURL),
|
||||||
|
slog.String("feed_icon_url", c.feed.IconURL),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
if err := c.store.StoreFeedIcon(c.feed.ID, icon); err != nil {
|
||||||
|
slog.Error("Unable to store feed icon",
|
||||||
|
slog.Int64("feed_id", c.feed.ID),
|
||||||
|
slog.String("website_url", c.feed.FeedURL),
|
||||||
|
slog.String("feed_icon_url", c.feed.IconURL),
|
||||||
|
slog.Any("error", err),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
slog.Debug("Feed icon stored",
|
||||||
|
slog.Int64("feed_id", c.feed.ID),
|
||||||
|
slog.String("website_url", c.feed.FeedURL),
|
||||||
|
slog.String("feed_icon_url", c.feed.IconURL),
|
||||||
|
slog.Int64("icon_id", icon.ID),
|
||||||
|
slog.String("icon_hash", icon.Hash),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *IconChecker) CreateFeedIconIfMissing() {
|
||||||
|
if c.store.HasFeedIcon(c.feed.ID) {
|
||||||
|
slog.Debug("Feed icon already exists",
|
||||||
|
slog.Int64("feed_id", c.feed.ID),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.fetchAndStoreIcon()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *IconChecker) UpdateOrCreateFeedIcon() {
|
||||||
|
c.fetchAndStoreIcon()
|
||||||
|
}
|
|
@ -11,8 +11,8 @@ import (
|
||||||
"miniflux.app/v2/internal/model"
|
"miniflux.app/v2/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HasIcon checks if the given feed has an icon.
|
// HasFeedIcon checks if the given feed has an icon.
|
||||||
func (s *Storage) HasIcon(feedID int64) bool {
|
func (s *Storage) HasFeedIcon(feedID int64) bool {
|
||||||
var result bool
|
var result bool
|
||||||
query := `SELECT true FROM feed_icons WHERE feed_id=$1`
|
query := `SELECT true FROM feed_icons WHERE feed_id=$1`
|
||||||
s.db.QueryRow(query, feedID).Scan(&result)
|
s.db.QueryRow(query, feedID).Scan(&result)
|
||||||
|
@ -57,59 +57,50 @@ func (s *Storage) IconByFeedID(userID, feedID int64) (*model.Icon, error) {
|
||||||
return &icon, nil
|
return &icon, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IconByHash returns an icon by the hash (checksum).
|
// StoreFeedIcon creates or updates a feed icon.
|
||||||
func (s *Storage) IconByHash(icon *model.Icon) error {
|
func (s *Storage) StoreFeedIcon(feedID int64, icon *model.Icon) error {
|
||||||
err := s.db.QueryRow(`SELECT id FROM icons WHERE hash=$1`, icon.Hash).Scan(&icon.ID)
|
tx, err := s.db.Begin()
|
||||||
if err == sql.ErrNoRows {
|
if err != nil {
|
||||||
return nil
|
return fmt.Errorf(`store: unable to start transaction: %v`, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.QueryRow(`SELECT id FROM icons WHERE hash=$1`, icon.Hash).Scan(&icon.ID); err == sql.ErrNoRows {
|
||||||
|
query := `
|
||||||
|
INSERT INTO icons
|
||||||
|
(hash, mime_type, content)
|
||||||
|
VALUES
|
||||||
|
($1, $2, $3)
|
||||||
|
RETURNING
|
||||||
|
id
|
||||||
|
`
|
||||||
|
err := tx.QueryRow(
|
||||||
|
query,
|
||||||
|
icon.Hash,
|
||||||
|
normalizeMimeType(icon.MimeType),
|
||||||
|
icon.Content,
|
||||||
|
).Scan(&icon.ID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf(`store: unable to create icon: %v`, err)
|
||||||
|
}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
return fmt.Errorf(`store: unable to fetch icon by hash %q: %v`, icon.Hash, err)
|
return fmt.Errorf(`store: unable to fetch icon by hash %q: %v`, icon.Hash, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if _, err := tx.Exec(`DELETE FROM feed_icons WHERE feed_id=$1`, feedID); err != nil {
|
||||||
}
|
tx.Rollback()
|
||||||
|
return fmt.Errorf(`store: unable to delete feed icon: %v`, err)
|
||||||
// CreateIcon creates a new icon.
|
|
||||||
func (s *Storage) CreateIcon(icon *model.Icon) error {
|
|
||||||
query := `
|
|
||||||
INSERT INTO icons
|
|
||||||
(hash, mime_type, content)
|
|
||||||
VALUES
|
|
||||||
($1, $2, $3)
|
|
||||||
RETURNING
|
|
||||||
id
|
|
||||||
`
|
|
||||||
err := s.db.QueryRow(
|
|
||||||
query,
|
|
||||||
icon.Hash,
|
|
||||||
normalizeMimeType(icon.MimeType),
|
|
||||||
icon.Content,
|
|
||||||
).Scan(&icon.ID)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf(`store: unable to create icon: %v`, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if _, err := tx.Exec(`INSERT INTO feed_icons (feed_id, icon_id) VALUES ($1, $2)`, feedID, icon.ID); err != nil {
|
||||||
}
|
tx.Rollback()
|
||||||
|
return fmt.Errorf(`store: unable to associate feed and icon: %v`, err)
|
||||||
// CreateFeedIcon creates an icon and associate the icon to the given feed.
|
|
||||||
func (s *Storage) CreateFeedIcon(feedID int64, icon *model.Icon) error {
|
|
||||||
err := s.IconByHash(icon)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if icon.ID == 0 {
|
if err := tx.Commit(); err != nil {
|
||||||
err := s.CreateIcon(icon)
|
return fmt.Errorf(`store: unable to commit transaction: %v`, err)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.db.Exec(`INSERT INTO feed_icons (feed_id, icon_id) VALUES ($1, $2)`, feedID, icon.ID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf(`store: unable to create feed icon: %v`, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue