Support Favicon URLs and auto-discoveryUnverified
5c037cb parent: a285def modified
internal/db/store.go +1 -0 | @@ -29,6 +29,7 @@ func (a *FeedStoreAdapter) GetFeedsToFetch(ctx context.Context, olderThan time.D | ||
| 29 | 29 | SiteURL: df.SiteURL.String, |
| 30 | 30 | Description: df.Description.String, |
| 31 | 31 | Type: df.FeedType.String, |
| 32 | + FaviconURL: df.FaviconURL.String, | |
| 32 | 33 | ETag: df.Etag.String, |
| 33 | 34 | LastModified: df.LastModified.String, |
| 34 | 35 | }) |
| @@ -29,6 +29,7 @@ func (a *FeedStoreAdapter) GetFeedsToFetch(ctx context.Context, olderThan time.D | |||
| 29 | SiteURL: df.SiteURL.String, | 29 | SiteURL: df.SiteURL.String, |
| 30 | Description: df.Description.String, | 30 | Description: df.Description.String, |
| 31 | Type: df.FeedType.String, | 31 | Type: df.FeedType.String, |
| 32 | + FaviconURL: df.FaviconURL.String, | ||
| 32 | ETag: df.Etag.String, | 33 | ETag: df.Etag.String, |
| 33 | LastModified: df.LastModified.String, | 34 | LastModified: df.LastModified.String, |
| 34 | }) | 35 | }) |
modified
internal/feed/fetcher.go +8 -2 | @@ -182,9 +182,15 @@ func (s *Scheduler) FetchFeed(ctx context.Context, feed *Feed) { | ||
| 182 | 182 | s.logger.Error("failed to update feed fetch result", "error", err, "feed", feed.URL) |
| 183 | 183 | } |
| 184 | 184 | |
| 185 | - if feed.SiteURL != "" { | |
| 185 | + if result != nil && result.Feed.FaviconURL != "" { | |
| 186 | + _ = s.store.UpdateFeedFavicon(ctx, feed.URL, result.Feed.FaviconURL) | |
| 187 | + } else if feed.FaviconURL == "" { | |
| 188 | + siteURL := feed.SiteURL | |
| 189 | + if siteURL == "" { | |
| 190 | + siteURL = feed.URL | |
| 191 | + } | |
| 186 | 192 | go func() { |
| 187 | - discResult, err := Discover(context.Background(), feed.SiteURL) | |
| 193 | + discResult, err := Discover(context.Background(), siteURL) | |
| 188 | 194 | if err == nil && discResult.Favicon != "" { |
| 189 | 195 | _ = s.store.UpdateFeedFavicon(context.Background(), feed.URL, discResult.Favicon) |
| 190 | 196 | } |
| @@ -182,9 +182,15 @@ func (s *Scheduler) FetchFeed(ctx context.Context, feed *Feed) { | |||
| 182 | s.logger.Error("failed to update feed fetch result", "error", err, "feed", feed.URL) | 182 | s.logger.Error("failed to update feed fetch result", "error", err, "feed", feed.URL) |
| 183 | } | 183 | } |
| 184 | 184 | ||
| 185 | - if feed.SiteURL != "" { | 185 | + if result != nil && result.Feed.FaviconURL != "" { |
| 186 | + _ = s.store.UpdateFeedFavicon(ctx, feed.URL, result.Feed.FaviconURL) | ||
| 187 | + } else if feed.FaviconURL == "" { | ||
| 188 | + siteURL := feed.SiteURL | ||
| 189 | + if siteURL == "" { | ||
| 190 | + siteURL = feed.URL | ||
| 191 | + } | ||
| 186 | go func() { | 192 | go func() { |
| 187 | - discResult, err := Discover(context.Background(), feed.SiteURL) | 193 | + discResult, err := Discover(context.Background(), siteURL) |
| 188 | if err == nil && discResult.Favicon != "" { | 194 | if err == nil && discResult.Favicon != "" { |
| 189 | _ = s.store.UpdateFeedFavicon(context.Background(), feed.URL, discResult.Favicon) | 195 | _ = s.store.UpdateFeedFavicon(context.Background(), feed.URL, discResult.Favicon) |
| 190 | } | 196 | } |
modified
internal/feed/parser.go +16 -1 | @@ -17,6 +17,7 @@ type Feed struct { | ||
| 17 | 17 | SiteURL string |
| 18 | 18 | Description string |
| 19 | 19 | Type string |
| 20 | + FaviconURL string | |
| 20 | 21 | ETag string |
| 21 | 22 | LastModified string |
| 22 | 23 | } |
| @@ -44,7 +45,10 @@ type rssFeed struct { | ||
| 44 | 45 | Title string `xml:"title"` |
| 45 | 46 | Link string `xml:"link"` |
| 46 | 47 | Description string `xml:"description"` |
| 47 | - Items []struct { | |
| 48 | + Image struct { | |
| 49 | + URL string `xml:"url"` | |
| 50 | + } `xml:"image"` | |
| 51 | + Items []struct { | |
| 48 | 52 | Title string `xml:"title"` |
| 49 | 53 | Link string `xml:"link"` |
| 50 | 54 | GUID string `xml:"guid"` |
| @@ -65,6 +69,8 @@ type atomFeed struct { | ||
| 65 | 69 | XMLName xml.Name `xml:"feed"` |
| 66 | 70 | Title string `xml:"title"` |
| 67 | 71 | Link []atomLink `xml:"link"` |
| 72 | + Icon string `xml:"icon"` | |
| 73 | + Logo string `xml:"logo"` | |
| 68 | 74 | Subtitle string `xml:"subtitle"` |
| 69 | 75 | Entry []struct { |
| 70 | 76 | Title string `xml:"title"` |
| @@ -103,6 +109,7 @@ type jsonFeed struct { | ||
| 103 | 109 | Title string `json:"title"` |
| 104 | 110 | HomePageURL string `json:"home_page_url"` |
| 105 | 111 | Description string `json:"description"` |
| 112 | + Favicon string `json:"favicon"` | |
| 106 | 113 | Items []struct { |
| 107 | 114 | ID string `json:"id"` |
| 108 | 115 | URL string `json:"url"` |
| @@ -144,6 +151,7 @@ func parseJSONFeed(data []byte, feedURL string) (*ParseResult, error) { | ||
| 144 | 151 | Title: jf.Title, |
| 145 | 152 | SiteURL: jf.HomePageURL, |
| 146 | 153 | Description: jf.Description, |
| 154 | + FaviconURL: jf.Favicon, | |
| 147 | 155 | Type: "json", |
| 148 | 156 | }, |
| 149 | 157 | } |
| @@ -214,6 +222,7 @@ func convertRSS(rss *rssFeed, feedURL string) *ParseResult { | ||
| 214 | 222 | Title: rss.Channel.Title, |
| 215 | 223 | SiteURL: rss.Channel.Link, |
| 216 | 224 | Description: rss.Channel.Description, |
| 225 | + FaviconURL: rss.Channel.Image.URL, | |
| 217 | 226 | Type: "rss", |
| 218 | 227 | }, |
| 219 | 228 | } |
| @@ -272,12 +281,18 @@ func convertRDF(rdf *rdfFeed, feedURL string) *ParseResult { | ||
| 272 | 281 | } |
| 273 | 282 | |
| 274 | 283 | func convertAtom(atom *atomFeed, feedURL string) *ParseResult { |
| 284 | + favicon := atom.Icon | |
| 285 | + if favicon == "" { | |
| 286 | + favicon = atom.Logo | |
| 287 | + } | |
| 288 | + | |
| 275 | 289 | result := &ParseResult{ |
| 276 | 290 | Feed: Feed{ |
| 277 | 291 | URL: feedURL, |
| 278 | 292 | Title: atom.Title, |
| 279 | 293 | SiteURL: pickAtomLink(atom.Link), |
| 280 | 294 | Description: atom.Subtitle, |
| 295 | + FaviconURL: favicon, | |
| 281 | 296 | Type: "atom", |
| 282 | 297 | }, |
| 283 | 298 | } |
| @@ -17,6 +17,7 @@ type Feed struct { | |||
| 17 | SiteURL string | 17 | SiteURL string |
| 18 | Description string | 18 | Description string |
| 19 | Type string | 19 | Type string |
| 20 | + FaviconURL string | ||
| 20 | ETag string | 21 | ETag string |
| 21 | LastModified string | 22 | LastModified string |
| 22 | } | 23 | } |
| @@ -44,7 +45,10 @@ type rssFeed struct { | |||
| 44 | Title string `xml:"title"` | 45 | Title string `xml:"title"` |
| 45 | Link string `xml:"link"` | 46 | Link string `xml:"link"` |
| 46 | Description string `xml:"description"` | 47 | Description string `xml:"description"` |
| 47 | - Items []struct { | 48 | + Image struct { |
| 49 | + URL string `xml:"url"` | ||
| 50 | + } `xml:"image"` | ||
| 51 | + Items []struct { | ||
| 48 | Title string `xml:"title"` | 52 | Title string `xml:"title"` |
| 49 | Link string `xml:"link"` | 53 | Link string `xml:"link"` |
| 50 | GUID string `xml:"guid"` | 54 | GUID string `xml:"guid"` |
| @@ -65,6 +69,8 @@ type atomFeed struct { | |||
| 65 | XMLName xml.Name `xml:"feed"` | 69 | XMLName xml.Name `xml:"feed"` |
| 66 | Title string `xml:"title"` | 70 | Title string `xml:"title"` |
| 67 | Link []atomLink `xml:"link"` | 71 | Link []atomLink `xml:"link"` |
| 72 | + Icon string `xml:"icon"` | ||
| 73 | + Logo string `xml:"logo"` | ||
| 68 | Subtitle string `xml:"subtitle"` | 74 | Subtitle string `xml:"subtitle"` |
| 69 | Entry []struct { | 75 | Entry []struct { |
| 70 | Title string `xml:"title"` | 76 | Title string `xml:"title"` |
| @@ -103,6 +109,7 @@ type jsonFeed struct { | |||
| 103 | Title string `json:"title"` | 109 | Title string `json:"title"` |
| 104 | HomePageURL string `json:"home_page_url"` | 110 | HomePageURL string `json:"home_page_url"` |
| 105 | Description string `json:"description"` | 111 | Description string `json:"description"` |
| 112 | + Favicon string `json:"favicon"` | ||
| 106 | Items []struct { | 113 | Items []struct { |
| 107 | ID string `json:"id"` | 114 | ID string `json:"id"` |
| 108 | URL string `json:"url"` | 115 | URL string `json:"url"` |
| @@ -144,6 +151,7 @@ func parseJSONFeed(data []byte, feedURL string) (*ParseResult, error) { | |||
| 144 | Title: jf.Title, | 151 | Title: jf.Title, |
| 145 | SiteURL: jf.HomePageURL, | 152 | SiteURL: jf.HomePageURL, |
| 146 | Description: jf.Description, | 153 | Description: jf.Description, |
| 154 | + FaviconURL: jf.Favicon, | ||
| 147 | Type: "json", | 155 | Type: "json", |
| 148 | }, | 156 | }, |
| 149 | } | 157 | } |
| @@ -214,6 +222,7 @@ func convertRSS(rss *rssFeed, feedURL string) *ParseResult { | |||
| 214 | Title: rss.Channel.Title, | 222 | Title: rss.Channel.Title, |
| 215 | SiteURL: rss.Channel.Link, | 223 | SiteURL: rss.Channel.Link, |
| 216 | Description: rss.Channel.Description, | 224 | Description: rss.Channel.Description, |
| 225 | + FaviconURL: rss.Channel.Image.URL, | ||
| 217 | Type: "rss", | 226 | Type: "rss", |
| 218 | }, | 227 | }, |
| 219 | } | 228 | } |
| @@ -272,12 +281,18 @@ func convertRDF(rdf *rdfFeed, feedURL string) *ParseResult { | |||
| 272 | } | 281 | } |
| 273 | 282 | ||
| 274 | func convertAtom(atom *atomFeed, feedURL string) *ParseResult { | 283 | func convertAtom(atom *atomFeed, feedURL string) *ParseResult { |
| 284 | + favicon := atom.Icon | ||
| 285 | + if favicon == "" { | ||
| 286 | + favicon = atom.Logo | ||
| 287 | + } | ||
| 288 | + | ||
| 275 | result := &ParseResult{ | 289 | result := &ParseResult{ |
| 276 | Feed: Feed{ | 290 | Feed: Feed{ |
| 277 | URL: feedURL, | 291 | URL: feedURL, |
| 278 | Title: atom.Title, | 292 | Title: atom.Title, |
| 279 | SiteURL: pickAtomLink(atom.Link), | 293 | SiteURL: pickAtomLink(atom.Link), |
| 280 | Description: atom.Subtitle, | 294 | Description: atom.Subtitle, |
| 295 | + FaviconURL: favicon, | ||
| 281 | Type: "atom", | 296 | Type: "atom", |
| 282 | }, | 297 | }, |
| 283 | } | 298 | } |
modified
internal/server/feeds_handler.go +13 -3 | @@ -87,10 +87,11 @@ func (s *Server) handleAddFeed(w http.ResponseWriter, r *http.Request) { | ||
| 87 | 87 | return |
| 88 | 88 | } |
| 89 | 89 | |
| 90 | - siteURL := result.Feed.SiteURL | |
| 91 | - if siteURL != "" { | |
| 90 | + if result.Feed.FaviconURL != "" { | |
| 91 | + _ = s.db.UpdateFeedFavicon(r.Context(), feedURL, result.Feed.FaviconURL) | |
| 92 | + } else if result.Feed.SiteURL != "" { | |
| 92 | 93 | go func() { |
| 93 | - discResult, err := feed.Discover(context.Background(), siteURL) | |
| 94 | + discResult, err := feed.Discover(context.Background(), result.Feed.SiteURL) | |
| 94 | 95 | if err == nil && discResult.Favicon != "" { |
| 95 | 96 | _ = s.db.UpdateFeedFavicon(context.Background(), feedURL, discResult.Favicon) |
| 96 | 97 | } |
| @@ -239,6 +240,15 @@ func (s *Server) handleOPMLUpload(w http.ResponseWriter, r *http.Request) { | ||
| 239 | 240 | continue |
| 240 | 241 | } |
| 241 | 242 | |
| 243 | + if fu.SiteURL != "" { | |
| 244 | + go func(feedURL, siteURL string) { | |
| 245 | + discResult, err := feed.Discover(context.Background(), siteURL) | |
| 246 | + if err == nil && discResult.Favicon != "" { | |
| 247 | + _ = s.db.UpdateFeedFavicon(context.Background(), feedURL, discResult.Favicon) | |
| 248 | + } | |
| 249 | + }(fu.URL, fu.SiteURL) | |
| 250 | + } | |
| 251 | + | |
| 242 | 252 | var subURI, subCID string |
| 243 | 253 | if client != nil { |
| 244 | 254 | record := atproto.SubscriptionRecord{ |
| @@ -87,10 +87,11 @@ func (s *Server) handleAddFeed(w http.ResponseWriter, r *http.Request) { | |||
| 87 | return | 87 | return |
| 88 | } | 88 | } |
| 89 | 89 | ||
| 90 | - siteURL := result.Feed.SiteURL | 90 | + if result.Feed.FaviconURL != "" { |
| 91 | - if siteURL != "" { | 91 | + _ = s.db.UpdateFeedFavicon(r.Context(), feedURL, result.Feed.FaviconURL) |
| 92 | + } else if result.Feed.SiteURL != "" { | ||
| 92 | go func() { | 93 | go func() { |
| 93 | - discResult, err := feed.Discover(context.Background(), siteURL) | 94 | + discResult, err := feed.Discover(context.Background(), result.Feed.SiteURL) |
| 94 | if err == nil && discResult.Favicon != "" { | 95 | if err == nil && discResult.Favicon != "" { |
| 95 | _ = s.db.UpdateFeedFavicon(context.Background(), feedURL, discResult.Favicon) | 96 | _ = s.db.UpdateFeedFavicon(context.Background(), feedURL, discResult.Favicon) |
| 96 | } | 97 | } |
| @@ -239,6 +240,15 @@ func (s *Server) handleOPMLUpload(w http.ResponseWriter, r *http.Request) { | |||
| 239 | continue | 240 | continue |
| 240 | } | 241 | } |
| 241 | 242 | ||
| 243 | + if fu.SiteURL != "" { | ||
| 244 | + go func(feedURL, siteURL string) { | ||
| 245 | + discResult, err := feed.Discover(context.Background(), siteURL) | ||
| 246 | + if err == nil && discResult.Favicon != "" { | ||
| 247 | + _ = s.db.UpdateFeedFavicon(context.Background(), feedURL, discResult.Favicon) | ||
| 248 | + } | ||
| 249 | + }(fu.URL, fu.SiteURL) | ||
| 250 | + } | ||
| 251 | + | ||
| 242 | var subURI, subCID string | 252 | var subURI, subCID string |
| 243 | if client != nil { | 253 | if client != nil { |
| 244 | record := atproto.SubscriptionRecord{ | 254 | record := atproto.SubscriptionRecord{ |