Add favicon as profile picture for feed accounts
Fetches the site's favicon and uses it as the avatar when creating or updating feed account profiles. Tries common favicon locations (/favicon.ico, /favicon.png, /apple-touch-icon.png) then falls back to Google's favicon service. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+22
-5
@@ -227,7 +227,15 @@ func (c *Crawler) StartPublishLoop() {
|
|||||||
if len(description) > 256 {
|
if len(description) > 256 {
|
||||||
description = description[:253] + "..."
|
description = description[:253] + "..."
|
||||||
}
|
}
|
||||||
if err := publisher.UpdateProfile(session, displayName, description, nil); err != nil {
|
// Fetch and upload favicon as avatar
|
||||||
|
var avatar *BlobRef
|
||||||
|
if feedInfo.SiteURL != "" {
|
||||||
|
faviconURL := publisher.FetchFavicon(feedInfo.SiteURL)
|
||||||
|
if faviconURL != "" {
|
||||||
|
avatar = publisher.fetchAndUploadImage(session, faviconURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := publisher.UpdateProfile(session, displayName, description, avatar); err != nil {
|
||||||
fmt.Printf("Publish: failed to set profile for %s: %v\n", account, err)
|
fmt.Printf("Publish: failed to set profile for %s: %v\n", account, err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("Publish: set profile for %s\n", account)
|
fmt.Printf("Publish: set profile for %s\n", account)
|
||||||
@@ -297,7 +305,7 @@ func (c *Crawler) getFeedInfo(feedURL string) *FeedInfo {
|
|||||||
// RefreshAllProfiles updates profiles for all existing accounts with feed URLs
|
// RefreshAllProfiles updates profiles for all existing accounts with feed URLs
|
||||||
func (c *Crawler) RefreshAllProfiles(publisher *Publisher, feedPassword string) {
|
func (c *Crawler) RefreshAllProfiles(publisher *Publisher, feedPassword string) {
|
||||||
rows, err := c.db.Query(`
|
rows, err := c.db.Query(`
|
||||||
SELECT url, title, description, publish_account
|
SELECT url, title, description, site_url, publish_account
|
||||||
FROM feeds
|
FROM feeds
|
||||||
WHERE publish_account IS NOT NULL AND publish_account <> ''
|
WHERE publish_account IS NOT NULL AND publish_account <> ''
|
||||||
`)
|
`)
|
||||||
@@ -309,8 +317,8 @@ func (c *Crawler) RefreshAllProfiles(publisher *Publisher, feedPassword string)
|
|||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var feedURL, account string
|
var feedURL, account string
|
||||||
var title, description *string
|
var title, description, siteURL *string
|
||||||
if err := rows.Scan(&feedURL, &title, &description, &account); err != nil {
|
if err := rows.Scan(&feedURL, &title, &description, &siteURL, &account); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,7 +350,16 @@ func (c *Crawler) RefreshAllProfiles(publisher *Publisher, feedPassword string)
|
|||||||
desc = desc[:253] + "..."
|
desc = desc[:253] + "..."
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := publisher.UpdateProfile(session, displayName, desc, nil); err != nil {
|
// Fetch and upload favicon as avatar
|
||||||
|
var avatar *BlobRef
|
||||||
|
if siteURL != nil && *siteURL != "" {
|
||||||
|
faviconURL := publisher.FetchFavicon(*siteURL)
|
||||||
|
if faviconURL != "" {
|
||||||
|
avatar = publisher.fetchAndUploadImage(session, faviconURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := publisher.UpdateProfile(session, displayName, desc, avatar); err != nil {
|
||||||
fmt.Printf("RefreshProfiles: update failed for %s: %v\n", account, err)
|
fmt.Printf("RefreshProfiles: update failed for %s: %v\n", account, err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("RefreshProfiles: updated %s\n", account)
|
fmt.Printf("RefreshProfiles: updated %s\n", account)
|
||||||
|
|||||||
@@ -473,6 +473,47 @@ func (p *Publisher) uploadImages(session *PDSSession, imageURLs []string, altTex
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fetchAndUploadImage downloads an image and uploads it to the PDS
|
// fetchAndUploadImage downloads an image and uploads it to the PDS
|
||||||
|
// FetchFavicon tries to get a favicon URL for a site
|
||||||
|
// Returns the favicon URL or empty string if not found
|
||||||
|
func (p *Publisher) FetchFavicon(siteURL string) string {
|
||||||
|
if siteURL == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the site URL to get the host
|
||||||
|
if !strings.Contains(siteURL, "://") {
|
||||||
|
siteURL = "https://" + siteURL
|
||||||
|
}
|
||||||
|
u, err := url.Parse(siteURL)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try common favicon locations
|
||||||
|
faviconURLs := []string{
|
||||||
|
fmt.Sprintf("https://%s/favicon.ico", u.Host),
|
||||||
|
fmt.Sprintf("https://%s/favicon.png", u.Host),
|
||||||
|
fmt.Sprintf("https://%s/apple-touch-icon.png", u.Host),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, faviconURL := range faviconURLs {
|
||||||
|
resp, err := p.httpClient.Head(faviconURL)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
contentType := resp.Header.Get("Content-Type")
|
||||||
|
if strings.HasPrefix(contentType, "image/") || strings.HasSuffix(faviconURL, ".ico") {
|
||||||
|
return faviconURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to Google's favicon service (reliable, returns PNG)
|
||||||
|
return fmt.Sprintf("https://www.google.com/s2/favicons?domain=%s&sz=128", u.Host)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Publisher) fetchAndUploadImage(session *PDSSession, imageURL string) *BlobRef {
|
func (p *Publisher) fetchAndUploadImage(session *PDSSession, imageURL string) *BlobRef {
|
||||||
// Fetch the image
|
// Fetch the image
|
||||||
resp, err := p.httpClient.Get(imageURL)
|
resp, err := p.httpClient.Get(imageURL)
|
||||||
|
|||||||
Reference in New Issue
Block a user