Added default globe.svg for invalid favicons
Some checks failed
Run Integration Tests / test (push) Failing after 41s
Some checks failed
Run Integration Tests / test (push) Failing after 41s
This commit is contained in:
parent
66414952e8
commit
5032173609
8 changed files with 118 additions and 51 deletions
|
@ -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)
|
||||
}
|
||||
|
|
14
favicon.go
14
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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1850,4 +1850,8 @@ body, h1, p, a, input, button {
|
|||
.leaflet-control-attribution a {
|
||||
color: var(--link) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.favicon.globe-fallback {
|
||||
color: var(--font-fg);
|
||||
}
|
||||
|
|
3
static/images/globe.svg
Normal file
3
static/images/globe.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="currentColor">
|
||||
<path d="M480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-155.5t86-127Q252-817 325-848.5T480-880q83 0 155.5 31.5t127 86q54.5 54.5 86 127T880-480q0 82-31.5 155t-86 127.5q-54.5 54.5-127 86T480-80Zm0-82q26-36 45-75t31-83H404q12 44 31 83t45 75Zm-104-16q-18-33-31.5-68.5T322-320H204q29 50 72.5 87t99.5 55Zm208 0q56-18 99.5-55t72.5-87H638q-9 38-22.5 73.5T584-178ZM170-400h136q-3-20-4.5-39.5T300-480q0-21 1.5-40.5T306-560H170q-5 20-7.5 39.5T160-480q0 21 2.5 40.5T170-400Zm216 0h188q3-20 4.5-39.5T580-480q0-21-1.5-40.5T574-560H386q-3 20-4.5 39.5T380-480q0 21 1.5 40.5T386-400Zm268 0h136q5-20 7.5-39.5T800-480q0-21-2.5-40.5T790-560H654q3 20 4.5 39.5T660-480q0 21-1.5 40.5T654-400Zm-16-240h118q-29-50-72.5-87T584-782q18 33 31.5 68.5T638-640Zm-234 0h152q-12-44-31-83t-45-75q-26 36-45 75t-31 83Zm-200 0h118q9-38 22.5-73.5T376-782q-56 18-99.5 55T204-640Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 986 B |
|
@ -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 = `<path d="M480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-155.5t86-127Q252-817 325-848.5T480-880q83 0 155.5 31.5t127 86q54.5 54.5 86 127T880-480q0 82-31.5 155t-86 127.5q-54.5 54.5-127 86T480-80Zm0-82q26-36 45-75t31-83H404q12 44 31 83t45 75Zm-104-16q-18-33-31.5-68.5T322-320H204q29 50 72.5 87t99.5 55Zm208 0q56-18 99.5-55t72.5-87H638q-9 38-22.5 73.5T584-178ZM170-400h136q-3-20-4.5-39.5T300-480q0-21 1.5-40.5T306-560H170q-5 20-7.5 39.5T160-480q0 21 2.5 40.5T170-400Zm216 0h188q3-20 4.5-39.5T580-480q0-21-1.5-40.5T574-560H386q-3 20-4.5 39.5T380-480q0 21 1.5 40.5T386-400Zm268 0h136q5-20 7.5-39.5T800-480q0-21-2.5-40.5T790-560H654q3 20 4.5 39.5T660-480q0 21-1.5 40.5T654-400Zm-16-240h118q-29-50-72.5-87T584-782q18 33 31.5 68.5T638-640Zm-234 0h152q-12-44-31-83t-45-75q-26 36-45 75t-31 83Zm-200 0h118q9-38 22.5-73.5T376-782q-56 18-99.5 55T204-640Z"/>`;
|
||||
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;
|
||||
|
|
|
@ -149,7 +149,7 @@
|
|||
<img
|
||||
src="/static/images/placeholder.svg"
|
||||
data-id="{{ .FaviconID }}"
|
||||
data-full="/image/{{ .FaviconID }}_thumb.webp"
|
||||
data-full="/image/{{ .FaviconID }}_icon.webp"
|
||||
alt="🌐"
|
||||
class="favicon placeholder-img"
|
||||
/>
|
||||
|
|
2
text.go
2
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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue