forked from Mirrors/fusionx
Fix favicon fetching and caching improvements
- Introduced functionality to fetch favicons from website HTML and cache them. - Updated default favicon creation to use a new feed.png file, with a fallback to inline PNG data if the file is missing. - Improve favicon handling in the service to improve reliability and user experience.
This commit is contained in:
@@ -117,12 +117,12 @@ func Run(params Params) {
|
||||
return func(c echo.Context) error {
|
||||
method := c.Request().Method
|
||||
path := c.Request().URL.Path
|
||||
|
||||
|
||||
// Allow feed refresh in demo mode (read-only operation)
|
||||
if method == "POST" && path == "/api/feeds/refresh" {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
|
||||
if method == "POST" || method == "PATCH" || method == "DELETE" {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "Demo mode: write operations not allowed")
|
||||
}
|
||||
@@ -167,7 +167,6 @@ func Run(params Params) {
|
||||
authed.GET("/config", configAPIHandler.Get)
|
||||
authed.PATCH("/config", configAPIHandler.Update)
|
||||
|
||||
|
||||
var err error
|
||||
addr := fmt.Sprintf("%s:%d", params.Host, params.Port)
|
||||
if params.TLSCert != "" {
|
||||
|
||||
@@ -32,11 +32,11 @@ func (f *faviconAPI) ServeFavicon(c echo.Context) error {
|
||||
}
|
||||
|
||||
faviconPath := filepath.Join(f.cacheDir, filename)
|
||||
|
||||
|
||||
// If favicon doesn't exist, try to fetch it from the actual feed
|
||||
if _, err := os.Stat(faviconPath); os.IsNotExist(err) {
|
||||
requestedHash := strings.TrimSuffix(filename, ".png")
|
||||
|
||||
|
||||
// Find all feeds and check which one matches this hash
|
||||
feeds, err := f.feedRepo.FindByFaviconHash(requestedHash)
|
||||
if err == nil {
|
||||
@@ -54,13 +54,13 @@ func (f *faviconAPI) ServeFavicon(c echo.Context) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If we couldn't find and fetch a real favicon, create a default as last resort
|
||||
if _, err := f.faviconSvc.CreateDefaultFavicon(faviconPath); err != nil {
|
||||
return echo.NewHTTPError(http.StatusNotFound, "favicon not found")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
c.Response().Header().Set("Cache-Control", "public, max-age=86400")
|
||||
return c.File(faviconPath)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
--color-success-content: oklch(100% 0 0);
|
||||
--color-warning: oklch(76% 0.188 70.08);
|
||||
--color-warning-content: oklch(100% 0 0);
|
||||
--color-error: oklch(57% 0.245 27.325);
|
||||
--color-error: oklch(65% 0.25 25);
|
||||
--color-error-content: oklch(100% 0 0);
|
||||
--radius-selector: 0.25rem;
|
||||
--radius-field: 0.5rem;
|
||||
@@ -61,7 +61,7 @@
|
||||
--color-success-content: oklch(100% 0 0);
|
||||
--color-warning: oklch(79% 0.184 86.047);
|
||||
--color-warning-content: oklch(100% 0 0);
|
||||
--color-error: oklch(57% 0.245 27.325);
|
||||
--color-error: oklch(65% 0.25 25);
|
||||
--color-error-content: oklch(100% 0 0);
|
||||
--radius-selector: 0.25rem;
|
||||
--radius-field: 0.5rem;
|
||||
|
||||
BIN
frontend/static/feed.png
Normal file
BIN
frontend/static/feed.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
@@ -7,6 +7,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -120,6 +121,11 @@ func (s *Service) fetchAndCacheFavicon(hostname, cachePath string) (string, erro
|
||||
fmt.Sprintf("https://www.google.com/s2/favicons?sz=32&domain=%s", hostname),
|
||||
}
|
||||
|
||||
// First try to find favicons from the website's HTML
|
||||
if feedFavicons := s.findFaviconsFromWebsite(hostname); len(feedFavicons) > 0 {
|
||||
faviconURLs = append(feedFavicons, faviconURLs...)
|
||||
}
|
||||
|
||||
for _, faviconURL := range faviconURLs {
|
||||
if err := s.downloadFavicon(faviconURL, cachePath); err == nil {
|
||||
return cachePath, nil
|
||||
@@ -152,16 +158,76 @@ func (s *Service) downloadFavicon(faviconURL, cachePath string) error {
|
||||
}
|
||||
|
||||
func (s *Service) CreateDefaultFavicon(cachePath string) (string, error) {
|
||||
defaultFaviconData := []byte{
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52,
|
||||
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x91, 0x68,
|
||||
0x36, 0x00, 0x00, 0x00, 0x19, 0x74, 0x45, 0x58, 0x74, 0x53, 0x6F, 0x66, 0x74, 0x77, 0x61, 0x72,
|
||||
0x65, 0x00, 0x41, 0x64, 0x6F, 0x62, 0x65, 0x20, 0x49, 0x6D, 0x61, 0x67, 0x65, 0x52, 0x65, 0x61,
|
||||
0x64, 0x79, 0x71, 0xC9, 0x65, 0x3C, 0x00, 0x00, 0x00, 0x32, 0x49, 0x44, 0x41, 0x54, 0x78, 0xDA,
|
||||
0x62, 0xFC, 0x3F, 0x95, 0x9F, 0x01, 0x37, 0x60, 0x62, 0xC0, 0x0B, 0x46, 0xAA, 0x34, 0x40, 0x80,
|
||||
0x01, 0x00, 0x06, 0x50, 0x4E, 0x20, 0x3E, 0x28, 0x84, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E,
|
||||
0x44, 0xAE, 0x42, 0x60, 0x82,
|
||||
// Copy the feed.png file as the default favicon
|
||||
sourcePath := "feed.png"
|
||||
sourceFile, err := os.Open(sourcePath)
|
||||
if err != nil {
|
||||
// Fallback to a simple inline PNG if feed.png doesn't exist
|
||||
defaultFaviconData := []byte{
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
|
||||
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x91, 0x68,
|
||||
0x36, 0x00, 0x00, 0x00, 0x19, 0x74, 0x45, 0x58, 0x74, 0x53, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72,
|
||||
0x65, 0x00, 0x41, 0x64, 0x6f, 0x62, 0x65, 0x20, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x61,
|
||||
0x64, 0x79, 0x71, 0xc9, 0x65, 0x3c, 0x00, 0x00, 0x00, 0x32, 0x49, 0x44, 0x41, 0x54, 0x78, 0x44,
|
||||
0x62, 0xfc, 0x3f, 0x95, 0x9f, 0x01, 0x37, 0x60, 0x62, 0xc0, 0x0b, 0x46, 0xaa, 0x34, 0x40, 0x80,
|
||||
0x01, 0x00, 0x06, 0x50, 0x4e, 0x20, 0x3e, 0x28, 0x84, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e,
|
||||
0x44, 0xae, 0x42, 0x60, 0x82,
|
||||
}
|
||||
return cachePath, os.WriteFile(cachePath, defaultFaviconData, 0600)
|
||||
}
|
||||
defer sourceFile.Close()
|
||||
|
||||
// #nosec G304 - cachePath is constructed from sanitized hostname hash
|
||||
destFile, err := os.Create(cachePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
_, err = io.Copy(destFile, sourceFile)
|
||||
return cachePath, err
|
||||
}
|
||||
|
||||
func (s *Service) findFaviconsFromWebsite(hostname string) []string {
|
||||
websiteURL := fmt.Sprintf("https://%s", hostname)
|
||||
|
||||
resp, err := s.client.Get(websiteURL)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
return cachePath, os.WriteFile(cachePath, defaultFaviconData, 0600)
|
||||
// Read only the first 50KB to avoid reading entire large pages
|
||||
body := make([]byte, 51200)
|
||||
n, err := io.ReadFull(resp.Body, body)
|
||||
if err != nil && err != io.ErrUnexpectedEOF {
|
||||
return nil
|
||||
}
|
||||
content := string(body[:n])
|
||||
|
||||
// Look for favicon links in HTML head
|
||||
faviconRegex := regexp.MustCompile(`(?i)<link[^>]*rel=["'](?:icon|shortcut icon)["'][^>]*href=["']([^"']+)["'][^>]*>`)
|
||||
matches := faviconRegex.FindAllStringSubmatch(content, -1)
|
||||
|
||||
var faviconURLs []string
|
||||
for _, match := range matches {
|
||||
if len(match) > 1 {
|
||||
faviconURL := match[1]
|
||||
// Convert relative URLs to absolute
|
||||
if !strings.HasPrefix(faviconURL, "http") {
|
||||
baseURL, err := url.Parse(websiteURL)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
faviconURL = baseURL.ResolveReference(&url.URL{Path: faviconURL}).String()
|
||||
}
|
||||
faviconURLs = append(faviconURLs, faviconURL)
|
||||
}
|
||||
}
|
||||
|
||||
return faviconURLs
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user