diff --git a/cache-images.go b/cache-images.go index 3a52f0b..26e1abc 100644 --- a/cache-images.go +++ b/cache-images.go @@ -25,7 +25,7 @@ import ( var ( cachingImages = make(map[string]*sync.Mutex) cachingImagesMu sync.Mutex - cachingSemaphore = make(chan struct{}, 10) // Limit to 10 concurrent downloads + cachingSemaphore = make(chan struct{}, 30) // Limit to concurrent downloads invalidImageIDs = make(map[string]struct{}) invalidImageIDsMu sync.Mutex @@ -34,6 +34,7 @@ var ( func cacheImage(imageURL, filename, imageID string) (string, bool, error) { cacheDir := "image_cache" cachedImagePath := filepath.Join(cacheDir, filename) + tempImagePath := cachedImagePath + ".tmp" // Check if the image is already cached if _, err := os.Stat(cachedImagePath); err == nil { @@ -96,8 +97,15 @@ func cacheImage(imageURL, filename, imageID string) (string, bool, error) { os.Mkdir(cacheDir, os.ModePerm) } - // Save the SVG file as-is - err = os.WriteFile(cachedImagePath, data, 0644) + // Save the SVG file as-is to the temp path + err = os.WriteFile(tempImagePath, data, 0644) + if err != nil { + recordInvalidImageID(imageID) + return "", false, err + } + + // Atomically rename the temp file to the final cached image path + err = os.Rename(tempImagePath, cachedImagePath) if err != nil { recordInvalidImageID(imageID) return "", false, err @@ -141,17 +149,25 @@ func cacheImage(imageURL, filename, imageID string) (string, bool, error) { os.Mkdir(cacheDir, os.ModePerm) } - // Open the cached file for writing - outFile, err := os.Create(cachedImagePath) + // Open the temp file for writing + outFile, err := os.Create(tempImagePath) if err != nil { recordInvalidImageID(imageID) return "", false, err } - defer outFile.Close() - // Encode the image to WebP and save + // Encode the image to WebP and save to the temp file options := &webp.Options{Lossless: false, Quality: 80} err = webp.Encode(outFile, img, options) + if err != nil { + outFile.Close() + recordInvalidImageID(imageID) + return "", false, err + } + outFile.Close() + + // Atomically rename the temp file to the final cached image path + err = os.Rename(tempImagePath, cachedImagePath) if err != nil { recordInvalidImageID(imageID) return "", false, err diff --git a/templates/images.html b/templates/images.html index 646772a..5412051 100755 --- a/templates/images.html +++ b/templates/images.html @@ -246,11 +246,104 @@ let page = parseInt(document.getElementById('template-data').getAttribute('data-page')) || 1; let query = document.getElementById('template-data').getAttribute('data-query'); let hardCacheEnabled = document.getElementById('template-data').getAttribute('data-hard-cache-enabled') === 'true'; + let noMoreImages = false; // Flag to indicate if there are no more images to load let imageElements = []; let imageIds = []; - // Function to check image status + /** + * Function to handle image load errors with retry logic + * @param {HTMLElement} imgElement - The image element that failed to load + * @param {number} retryCount - Number of retries left + * @param {number} retryDelay - Delay between retries in milliseconds + */ + function handleImageError(imgElement, retryCount = 3, retryDelay = 1000) { + if (retryCount > 0) { + setTimeout(() => { + imgElement.src = imgElement.getAttribute('data-full'); + imgElement.onerror = function() { + handleImageError(imgElement, retryCount - 1, retryDelay); + }; + }, retryDelay); + } else { + // After retries, hide the image container or set a fallback image + console.warn('Image failed to load after retries:', imgElement.getAttribute('data-full')); + imgElement.parentElement.style.display = 'none'; // Hide the image container + // Alternatively, set a fallback image: + // imgElement.src = '/static/images/fallback.svg'; + } + } + + /** + * Function to ensure the page is scrollable by loading more images if necessary + */ + function ensureScrollable() { + if (noMoreImages) return; // Do not attempt if no more images are available + // Check if the page is not scrollable + if (document.body.scrollHeight <= window.innerHeight) { + // If not scrollable, fetch the next page + fetchNextPage(); + } + } + + /** + * Function to fetch the next page of images + */ + function fetchNextPage() { + if (isFetching || noMoreImages) return; + isFetching = true; + page += 1; + + fetch(`/search?q=${encodeURIComponent(query)}&t=image&p=${page}&ajax=true`) + .then(response => response.text()) + .then(html => { + // Parse the returned HTML and extract image elements + let parser = new DOMParser(); + let doc = parser.parseFromString(html, 'text/html'); + let newImages = doc.querySelectorAll('.image'); + + if (newImages.length > 0) { + let resultsContainer = document.querySelector('.images'); + newImages.forEach(imageDiv => { + // Append new images to the container + resultsContainer.appendChild(imageDiv); + + // Get the img element + let img = imageDiv.querySelector('img'); + if (img) { + if (hardCacheEnabled) { + // Replace image with placeholder + img.src = '/static/images/placeholder.svg'; + img.onerror = function() { + handleImageError(img); + }; + + let id = img.getAttribute('data-id'); + imageElements.push(img); + imageIds.push(id); + } + } + }); + if (hardCacheEnabled) { + checkImageStatus(); + } + // After appending new images, ensure the page is scrollable + ensureScrollable(); + } else { + // No more images to load + noMoreImages = true; + } + isFetching = false; + }) + .catch(error => { + console.error('Error fetching next page:', error); + isFetching = false; + }); + } + + /** + * Function to check image status via AJAX + */ function checkImageStatus() { if (!hardCacheEnabled) return; if (imageIds.length === 0) { @@ -259,7 +352,7 @@ } // Send AJAX request to check image status - fetch('/image_status?image_ids=' + imageIds.join(',')) + fetch(`/image_status?image_ids=${imageIds.join(',')}`) .then(response => response.json()) .then(statusMap => { imageElements = imageElements.filter(img => { @@ -267,12 +360,17 @@ if (statusMap[id]) { // Image is ready, update src img.src = statusMap[id]; + img.onerror = function() { + handleImageError(img); + }; // Remove the image id from the list imageIds = imageIds.filter(imageId => imageId !== id); return false; // Remove img from imageElements } return true; // Keep img in imageElements }); + // After updating images, ensure the page is scrollable + ensureScrollable(); }) .catch(error => { console.error('Error checking image status:', error); @@ -292,66 +390,25 @@ // Start checking image status let imageStatusTimer = setInterval(checkImageStatus, imageStatusInterval); checkImageStatus(); // Initial check + + // After initial images are loaded, ensure the page is scrollable + window.addEventListener('load', ensureScrollable); } // Infinite scrolling window.addEventListener('scroll', function() { - if (isFetching) return; + if (isFetching || noMoreImages) return; if (window.innerHeight + window.scrollY >= document.body.offsetHeight - scrollThreshold) { // User scrolled near the bottom - isFetching = true; - page += 1; - - fetch('/search?q=' + encodeURIComponent(query) + '&t=image&p=' + page + '&ajax=true') - .then(response => response.text()) - .then(html => { - // Parse the returned HTML and extract image elements - let parser = new DOMParser(); - let doc = parser.parseFromString(html, 'text/html'); - let newImages = doc.querySelectorAll('.image'); - - if (newImages.length > 0) { - let resultsContainer = document.querySelector('.images'); - newImages.forEach(imageDiv => { - // Append new images to the container - resultsContainer.appendChild(imageDiv); - - // Get the img element - let img = imageDiv.querySelector('img'); - if (img) { - if (hardCacheEnabled) { - // Replace image with placeholder - img.src = '/static/images/placeholder.svg'; - - let id = img.getAttribute('data-id'); - imageElements.push(img); - imageIds.push(id); - } - } - }); - if (hardCacheEnabled) { - checkImageStatus(); - } - isFetching = false; - } else { - // No more images to load - isFetching = false; - // Optionally, remove the scroll event listener if no more pages - // window.removeEventListener('scroll', scrollHandler); - } - }) - .catch(error => { - console.error('Error fetching next page:', error); - isFetching = false; - }); + fetchNextPage(); } }); // Remove 'js-enabled' class from content document.getElementById('content').classList.remove('js-enabled'); })(); - +