From 5032173609f14b16f132c1f3cac012bcc0d5851c Mon Sep 17 00:00:00 2001 From: partisan Date: Fri, 30 May 2025 23:14:49 +0200 Subject: [PATCH] Added default globe.svg for invalid favicons --- cache-images.go | 86 ++++++++++++++++++++----------- favicon.go | 14 ++--- images.go | 4 +- static/css/style.css | 6 ++- static/images/globe.svg | 3 ++ static/js/dynamicscrollingtext.js | 52 +++++++++++++++---- templates/text.html | 2 +- text.go | 2 +- 8 files changed, 118 insertions(+), 51 deletions(-) create mode 100644 static/images/globe.svg diff --git a/cache-images.go b/cache-images.go index 692476e..84a4256 100644 --- a/cache-images.go +++ b/cache-images.go @@ -36,7 +36,7 @@ var ( imageURLMapMu sync.RWMutex ) -func cacheImage(imageURL, imageID string, isThumbnail bool) (string, bool, error) { +func cacheImage(imageURL, imageID string, imageType string) (string, bool, error) { if imageURL == "" { recordInvalidImageID(imageID) return "", false, fmt.Errorf("empty image URL for image ID %s", imageID) @@ -44,10 +44,15 @@ func cacheImage(imageURL, imageID string, isThumbnail bool) (string, bool, error // Construct the filename based on the image ID and type var filename string - if isThumbnail { + switch imageType { + case "thumb": filename = fmt.Sprintf("%s_thumb.webp", imageID) - } else { + case "icon": + filename = fmt.Sprintf("%s_icon.webp", imageID) + case "full": filename = fmt.Sprintf("%s_full.webp", imageID) + default: + return "", false, fmt.Errorf("unknown image type: %s", imageType) } // Make sure we store inside: config.DriveCache.Path / images @@ -228,29 +233,23 @@ func handleImageServe(w http.ResponseWriter, r *http.Request) { // Adjust to read from config.DriveCache.Path / images cachedImagePath := filepath.Join(config.DriveCache.Path, "images", filename) - if hasExtension && imageType == "thumb" { - // Requesting cached image (thumbnail or full) + if hasExtension && (imageType == "thumb" || imageType == "icon") { if _, err := os.Stat(cachedImagePath); err == nil { - // Update the modification time to now - err := os.Chtimes(cachedImagePath, time.Now(), time.Now()) - if err != nil { - printWarn("Failed to update modification time for %s: %v", cachedImagePath, err) - } - - // Determine content type based on file extension - contentType := "image/webp" - w.Header().Set("Content-Type", contentType) + // Update the modification time + _ = os.Chtimes(cachedImagePath, time.Now(), time.Now()) + w.Header().Set("Content-Type", "image/webp") w.Header().Set("Cache-Control", "public, max-age=31536000") http.ServeFile(w, r, cachedImagePath) return } else { - // Cached image not found if config.DriveCacheEnabled { - // Thumbnail should be cached, but not found - serveMissingImage(w, r) + if imageType == "icon" { + serveGlobeImage(w, r) + } else { + serveMissingImage(w, r) + } return } - // Else, proceed to proxy if caching is disabled } } @@ -326,8 +325,12 @@ func handleImageStatus(w http.ResponseWriter, r *http.Request) { invalidImageIDsMu.Unlock() if isInvalid { - // Image is invalid; inform the frontend by setting the missing image URL - statusMap[id] = "/static/images/missing.svg" + // Image is invalid; provide appropriate fallback + if strings.HasSuffix(id, "_icon.webp") || strings.HasSuffix(id, "_icon") { + statusMap[id] = "/images/globe.svg" + } else { + statusMap[id] = "/images/missing.svg" + } continue } @@ -335,11 +338,15 @@ func handleImageStatus(w http.ResponseWriter, r *http.Request) { extensions := []string{"webp", "svg"} // Extensions without leading dots imageReady := false - // Check thumbnail first for _, ext := range extensions { - thumbFilename := fmt.Sprintf("%s_thumb.%s", id, ext) - thumbPath := filepath.Join(config.DriveCache.Path, "images", thumbFilename) + thumbPath := filepath.Join(config.DriveCache.Path, "images", fmt.Sprintf("%s_thumb.%s", id, ext)) + iconPath := filepath.Join(config.DriveCache.Path, "images", fmt.Sprintf("%s_icon.%s", id, ext)) + if _, err := os.Stat(iconPath); err == nil { + statusMap[id] = fmt.Sprintf("/image/%s_icon.%s", id, ext) + imageReady = true + break + } if _, err := os.Stat(thumbPath); err == nil { statusMap[id] = fmt.Sprintf("/image/%s_thumb.%s", id, ext) imageReady = true @@ -363,11 +370,13 @@ func handleImageStatus(w http.ResponseWriter, r *http.Request) { // If neither is ready and image is not invalid if !imageReady { - if !config.DriveCacheEnabled { - // Hard cache is disabled; use the proxy URL - statusMap[id] = fmt.Sprintf("/image/%s_thumb", id) + // Distinguish favicon vs image fallback + if strings.HasSuffix(id, "_icon.webp") || strings.HasSuffix(id, "_icon") { + statusMap[id] = "/images/globe.svg" + } else if !config.DriveCacheEnabled { + statusMap[id] = "/images/missing.svg" } - // Else, do not set statusMap[id]; the frontend will keep checking + // else: leave it unset — frontend will retry } } @@ -520,8 +529,25 @@ func serveMissingImage(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-store, must-revalidate") w.Header().Set("Pragma", "no-cache") w.Header().Set("Expires", "0") - if config.DriveCacheEnabled { - w.WriteHeader(http.StatusNotFound) - } http.ServeFile(w, r, missingImagePath) } + +func serveGlobeImage(w http.ResponseWriter, r *http.Request) { + globePath := filepath.Join("static", "images", "globe.svg") + + // Set error code FIRST + w.WriteHeader(http.StatusNotFound) + + // Now read the file and write it manually, to avoid conflict with http.ServeFile + data, err := os.ReadFile(globePath) + if err != nil { + http.Error(w, "globe.svg not found", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "image/svg+xml") + w.Header().Set("Cache-Control", "no-store, must-revalidate") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Expires", "0") + _, _ = w.Write(data) +} diff --git a/favicon.go b/favicon.go index 6b9dbf2..37c7d94 100644 --- a/favicon.go +++ b/favicon.go @@ -325,22 +325,22 @@ func findFaviconInHTML(pageURL string) string { func getFaviconProxyURL(rawFavicon, pageURL string) string { if pageURL == "" { - return "/static/images/missing.svg" + return "/static/images/globe.svg" } cacheID := faviconIDFromURL(pageURL) - filename := fmt.Sprintf("%s_thumb.webp", cacheID) + filename := fmt.Sprintf("%s_icon.webp", cacheID) cachedPath := filepath.Join(config.DriveCache.Path, "images", filename) if _, err := os.Stat(cachedPath); err == nil { - return fmt.Sprintf("/image/%s_thumb.webp", cacheID) + return fmt.Sprintf("/image/%s_icon.webp", cacheID) } // Resolve URL faviconURL, _ := resolveFaviconURL(rawFavicon, pageURL) if faviconURL == "" { recordInvalidImageID(cacheID) - return "/static/images/missing.svg" + return "/static/images/globe.svg" } // Check if already downloading @@ -361,10 +361,10 @@ func getFaviconProxyURL(rawFavicon, pageURL string) string { } } - return fmt.Sprintf("/image/%s_thumb.webp", cacheID) + return fmt.Sprintf("/image/%s_icon.webp", cacheID) } -// Caches favicon, always saving *_thumb.webp +// Caches favicon, always saving *_icon.webp func cacheFavicon(imageURL, imageID string) (string, bool, error) { // if imageURL == "" { // recordInvalidImageID(imageID) @@ -374,7 +374,7 @@ func cacheFavicon(imageURL, imageID string) (string, bool, error) { // Debug fmt.Printf("Downloading favicon [%s] for ID [%s]\n", imageURL, imageID) - filename := fmt.Sprintf("%s_thumb.webp", imageID) + filename := fmt.Sprintf("%s_icon.webp", imageID) imageCacheDir := filepath.Join(config.DriveCache.Path, "images") if err := os.MkdirAll(imageCacheDir, 0755); err != nil { return "", false, fmt.Errorf("couldn't create images folder: %v", err) diff --git a/images.go b/images.go index be7ed6e..ef03f8b 100755 --- a/images.go +++ b/images.go @@ -174,7 +174,7 @@ func fetchImageResults(query, safe, lang string, page int, synchronous bool) []I if config.DriveCacheEnabled { // Cache the thumbnail image asynchronously go func(imgResult ImageSearchResult) { - _, success, err := cacheImage(imgResult.Thumb, imgResult.ID, true) + _, success, err := cacheImage(imgResult.Thumb, imgResult.ID, "thumb") if err != nil || !success { printWarn("Failed to cache thumbnail image %s: %v", imgResult.Thumb, err) removeImageResultFromCache(query, page, safe == "active", lang, imgResult.ID) @@ -233,7 +233,7 @@ func fetchImageResults(query, safe, lang string, page int, synchronous bool) []I if config.DriveCacheEnabled { // Cache the thumbnail image asynchronously go func(imgResult ImageSearchResult) { - _, success, err := cacheImage(imgResult.Thumb, imgResult.ID, true) + _, success, err := cacheImage(imgResult.Thumb, imgResult.ID, "thumb") if err != nil || !success { printWarn("Failed to cache thumbnail image %s: %v", imgResult.Thumb, err) removeImageResultFromCache(query, page, safe == "active", lang, imgResult.ID) diff --git a/static/css/style.css b/static/css/style.css index f51b714..a477fa3 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -1850,4 +1850,8 @@ body, h1, p, a, input, button { .leaflet-control-attribution a { color: var(--link) !important; } -} \ No newline at end of file +} + +.favicon.globe-fallback { + color: var(--font-fg); +} diff --git a/static/images/globe.svg b/static/images/globe.svg new file mode 100644 index 0000000..4837352 --- /dev/null +++ b/static/images/globe.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/js/dynamicscrollingtext.js b/static/js/dynamicscrollingtext.js index 8181978..98811b5 100644 --- a/static/js/dynamicscrollingtext.js +++ b/static/js/dynamicscrollingtext.js @@ -28,26 +28,59 @@ imgElement.closest('.favicon-wrapper')?.classList.remove('loading'); if (title) title.classList.remove('title-loading'); - if (type === 'image' && imgElement.src.endsWith('/images/missing.svg')) { + if (type === 'image' && imgElement.src.endsWith('/images/globe.svg')) { container.remove(); } } // Handle image/favicon loading errors - function handleImageError(imgElement, retryCount = 10, retryDelay = 500) { + function handleImageError(imgElement, retryCount = 8, retryDelay = 500) { + const isFavicon = !!imgElement.closest('.favicon-wrapper'); const container = imgElement.closest(type === 'image' ? '.image' : '.result_item'); const titleSelector = type === 'image' ? '.img_title' : '.result-url'; const title = container?.querySelector(titleSelector); - - if (retryCount > 0) { - setTimeout(() => { - imgElement.src = imgElement.getAttribute('data-full'); - imgElement.onerror = () => handleImageError(imgElement, retryCount - 1, retryDelay); - }, retryDelay); + const fullURL = imgElement.getAttribute('data-full'); + + if (retryCount > 0 && !imgElement.dataset.checked404) { + imgElement.dataset.checked404 = '1'; // avoid infinite loop + + fetch(fullURL, { method: 'HEAD' }) + .then(res => { + if (res.status === 404) { + fallbackToGlobe(imgElement); + } else { + setTimeout(() => { + imgElement.src = fullURL; + imgElement.onerror = () => handleImageError(imgElement, retryCount - 1, retryDelay); + }, retryDelay); + } + }) + .catch(() => { + fallbackToGlobe(imgElement); + }); } else { + fallbackToGlobe(imgElement); + } + + function fallbackToGlobe(imgElement) { imgElement.closest('.favicon-wrapper')?.classList.remove('loading'); if (title) title.classList.remove('title-loading'); - if (type === 'image') container.style.display = 'none'; + + if (isFavicon) { + const wrapper = imgElement.closest('.favicon-wrapper') || imgElement.parentElement; + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("xmlns", "http://www.w3.org/2000/svg"); + svg.setAttribute("viewBox", "0 -960 960 960"); + svg.setAttribute("height", imgElement.height || "16"); + svg.setAttribute("width", imgElement.width || "16"); + svg.setAttribute("fill", "currentColor"); + svg.classList.add("favicon", "globe-fallback"); + svg.innerHTML = ``; + imgElement.remove(); + wrapper.appendChild(svg); + } else if (type === 'image') { + container?.remove(); + } } } @@ -128,6 +161,7 @@ group.forEach(id => { const elements = mediaMap.get(id); const resolved = statusMap[id]; + if (!elements) return; if (resolved && resolved !== 'pending') { elements.forEach(img => { img.src = resolved; diff --git a/templates/text.html b/templates/text.html index 6d7246c..74566dd 100755 --- a/templates/text.html +++ b/templates/text.html @@ -149,7 +149,7 @@ 🌐 diff --git a/text.go b/text.go index 63ce269..7cce267 100755 --- a/text.go +++ b/text.go @@ -103,7 +103,7 @@ func HandleTextSearch(w http.ResponseWriter, settings UserSettings, query string func ensureFaviconIsCached(faviconID, rootURL string) { // Check if already exists in cache - filename := fmt.Sprintf("%s_thumb.webp", faviconID) + filename := fmt.Sprintf("%s_icon.webp", faviconID) cachedPath := filepath.Join(config.DriveCache.Path, "images", filename) if _, err := os.Stat(cachedPath); err == nil {