nandi/gleanpublic⑂ Fork 0
⑂ 5c037cb
Commits
⬇ Clone ▾
git clone https://git.rickub.com/nandi/glean.git
git clone ssh://git@rickub.com/nandi/glean.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Support Favicon URLs and auto-discoveryUnverified

Julien Robert committed 2026-04-22T12:37:36+02:00 Browse files
5c037cb parent: a285def
modified internal/db/store.go +1 -0
@@ -29,6 +29,7 @@ func (a *FeedStoreAdapter) GetFeedsToFetch(ctx context.Context, olderThan time.D
2929 SiteURL: df.SiteURL.String,
3030 Description: df.Description.String,
3131 Type: df.FeedType.String,
32+ FaviconURL: df.FaviconURL.String,
3233 ETag: df.Etag.String,
3334 LastModified: df.LastModified.String,
3435 })
@@ -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) {
182182 s.logger.Error("failed to update feed fetch result", "error", err, "feed", feed.URL)
183183 }
184184
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+ }
186192 go func() {
187- discResult, err := Discover(context.Background(), feed.SiteURL)
193+ discResult, err := Discover(context.Background(), siteURL)
188194 if err == nil && discResult.Favicon != "" {
189195 _ = s.store.UpdateFeedFavicon(context.Background(), feed.URL, discResult.Favicon)
190196 }
@@ -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 {
1717 SiteURL string
1818 Description string
1919 Type string
20+ FaviconURL string
2021 ETag string
2122 LastModified string
2223 }
@@ -44,7 +45,10 @@ type rssFeed struct {
4445 Title string `xml:"title"`
4546 Link string `xml:"link"`
4647 Description string `xml:"description"`
47- Items []struct {
48+ Image struct {
49+ URL string `xml:"url"`
50+ } `xml:"image"`
51+ Items []struct {
4852 Title string `xml:"title"`
4953 Link string `xml:"link"`
5054 GUID string `xml:"guid"`
@@ -65,6 +69,8 @@ type atomFeed struct {
6569 XMLName xml.Name `xml:"feed"`
6670 Title string `xml:"title"`
6771 Link []atomLink `xml:"link"`
72+ Icon string `xml:"icon"`
73+ Logo string `xml:"logo"`
6874 Subtitle string `xml:"subtitle"`
6975 Entry []struct {
7076 Title string `xml:"title"`
@@ -103,6 +109,7 @@ type jsonFeed struct {
103109 Title string `json:"title"`
104110 HomePageURL string `json:"home_page_url"`
105111 Description string `json:"description"`
112+ Favicon string `json:"favicon"`
106113 Items []struct {
107114 ID string `json:"id"`
108115 URL string `json:"url"`
@@ -144,6 +151,7 @@ func parseJSONFeed(data []byte, feedURL string) (*ParseResult, error) {
144151 Title: jf.Title,
145152 SiteURL: jf.HomePageURL,
146153 Description: jf.Description,
154+ FaviconURL: jf.Favicon,
147155 Type: "json",
148156 },
149157 }
@@ -214,6 +222,7 @@ func convertRSS(rss *rssFeed, feedURL string) *ParseResult {
214222 Title: rss.Channel.Title,
215223 SiteURL: rss.Channel.Link,
216224 Description: rss.Channel.Description,
225+ FaviconURL: rss.Channel.Image.URL,
217226 Type: "rss",
218227 },
219228 }
@@ -272,12 +281,18 @@ func convertRDF(rdf *rdfFeed, feedURL string) *ParseResult {
272281 }
273282
274283 func convertAtom(atom *atomFeed, feedURL string) *ParseResult {
284+ favicon := atom.Icon
285+ if favicon == "" {
286+ favicon = atom.Logo
287+ }
288+
275289 result := &ParseResult{
276290 Feed: Feed{
277291 URL: feedURL,
278292 Title: atom.Title,
279293 SiteURL: pickAtomLink(atom.Link),
280294 Description: atom.Subtitle,
295+ FaviconURL: favicon,
281296 Type: "atom",
282297 },
283298 }
@@ -17,6 +17,7 @@ type Feed struct {
17 SiteURL string17 SiteURL string
18 Description string18 Description string
19 Type string19 Type string
20+ FaviconURL string
20 ETag string21 ETag string
21 LastModified string22 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) {
8787 return
8888 }
8989
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 != "" {
9293 go func() {
93- discResult, err := feed.Discover(context.Background(), siteURL)
94+ discResult, err := feed.Discover(context.Background(), result.Feed.SiteURL)
9495 if err == nil && discResult.Favicon != "" {
9596 _ = s.db.UpdateFeedFavicon(context.Background(), feedURL, discResult.Favicon)
9697 }
@@ -239,6 +240,15 @@ func (s *Server) handleOPMLUpload(w http.ResponseWriter, r *http.Request) {
239240 continue
240241 }
241242
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+
242252 var subURI, subCID string
243253 if client != nil {
244254 record := atproto.SubscriptionRecord{
@@ -87,10 +87,11 @@ func (s *Server) handleAddFeed(w http.ResponseWriter, r *http.Request) {
87 return87 return
88 }88 }
89 89
90- siteURL := result.Feed.SiteURL90+ 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 continue240 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 string252 var subURI, subCID string
243 if client != nil {253 if client != nil {
244 record := atproto.SubscriptionRecord{254 record := atproto.SubscriptionRecord{