Fix invalid image URLs & dynamic scrolling

This commit is contained in:
partisan 2024-11-14 16:33:04 +01:00
parent 01c48fd366
commit b6fd92673c
2 changed files with 129 additions and 56 deletions

View file

@ -25,7 +25,7 @@ import (
var ( var (
cachingImages = make(map[string]*sync.Mutex) cachingImages = make(map[string]*sync.Mutex)
cachingImagesMu 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{}) invalidImageIDs = make(map[string]struct{})
invalidImageIDsMu sync.Mutex invalidImageIDsMu sync.Mutex
@ -34,6 +34,7 @@ var (
func cacheImage(imageURL, filename, imageID string) (string, bool, error) { func cacheImage(imageURL, filename, imageID string) (string, bool, error) {
cacheDir := "image_cache" cacheDir := "image_cache"
cachedImagePath := filepath.Join(cacheDir, filename) cachedImagePath := filepath.Join(cacheDir, filename)
tempImagePath := cachedImagePath + ".tmp"
// Check if the image is already cached // Check if the image is already cached
if _, err := os.Stat(cachedImagePath); err == nil { 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) os.Mkdir(cacheDir, os.ModePerm)
} }
// Save the SVG file as-is // Save the SVG file as-is to the temp path
err = os.WriteFile(cachedImagePath, data, 0644) 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 { if err != nil {
recordInvalidImageID(imageID) recordInvalidImageID(imageID)
return "", false, err return "", false, err
@ -141,17 +149,25 @@ func cacheImage(imageURL, filename, imageID string) (string, bool, error) {
os.Mkdir(cacheDir, os.ModePerm) os.Mkdir(cacheDir, os.ModePerm)
} }
// Open the cached file for writing // Open the temp file for writing
outFile, err := os.Create(cachedImagePath) outFile, err := os.Create(tempImagePath)
if err != nil { if err != nil {
recordInvalidImageID(imageID) recordInvalidImageID(imageID)
return "", false, err 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} options := &webp.Options{Lossless: false, Quality: 80}
err = webp.Encode(outFile, img, options) 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 { if err != nil {
recordInvalidImageID(imageID) recordInvalidImageID(imageID)
return "", false, err return "", false, err

View file

@ -246,64 +246,55 @@
let page = parseInt(document.getElementById('template-data').getAttribute('data-page')) || 1; let page = parseInt(document.getElementById('template-data').getAttribute('data-page')) || 1;
let query = document.getElementById('template-data').getAttribute('data-query'); let query = document.getElementById('template-data').getAttribute('data-query');
let hardCacheEnabled = document.getElementById('template-data').getAttribute('data-hard-cache-enabled') === 'true'; 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 imageElements = [];
let imageIds = []; let imageIds = [];
// Function to check image status /**
function checkImageStatus() { * Function to handle image load errors with retry logic
if (!hardCacheEnabled) return; * @param {HTMLElement} imgElement - The image element that failed to load
if (imageIds.length === 0) { * @param {number} retryCount - Number of retries left
// No images to check, do nothing * @param {number} retryDelay - Delay between retries in milliseconds
return; */
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';
}
} }
// Send AJAX request to check image status /**
fetch('/image_status?image_ids=' + imageIds.join(',')) * Function to ensure the page is scrollable by loading more images if necessary
.then(response => response.json()) */
.then(statusMap => { function ensureScrollable() {
imageElements = imageElements.filter(img => { if (noMoreImages) return; // Do not attempt if no more images are available
let id = img.getAttribute('data-id'); // Check if the page is not scrollable
if (statusMap[id]) { if (document.body.scrollHeight <= window.innerHeight) {
// Image is ready, update src // If not scrollable, fetch the next page
img.src = statusMap[id]; fetchNextPage();
// 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
});
})
.catch(error => {
console.error('Error checking image status:', error);
});
} }
// Initialize imageElements and imageIds /**
if (hardCacheEnabled) { * Function to fetch the next page of images
imageElements = Array.from(document.querySelectorAll('img[data-id]')); */
imageIds = imageElements.map(img => img.getAttribute('data-id')); function fetchNextPage() {
if (isFetching || noMoreImages) return;
// Replace images with placeholders
imageElements.forEach(img => {
img.src = '/static/images/placeholder.svg';
});
// Start checking image status
let imageStatusTimer = setInterval(checkImageStatus, imageStatusInterval);
checkImageStatus(); // Initial check
}
// Infinite scrolling
window.addEventListener('scroll', function() {
if (isFetching) return;
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - scrollThreshold) {
// User scrolled near the bottom
isFetching = true; isFetching = true;
page += 1; page += 1;
fetch('/search?q=' + encodeURIComponent(query) + '&t=image&p=' + page + '&ajax=true') fetch(`/search?q=${encodeURIComponent(query)}&t=image&p=${page}&ajax=true`)
.then(response => response.text()) .then(response => response.text())
.then(html => { .then(html => {
// Parse the returned HTML and extract image elements // Parse the returned HTML and extract image elements
@ -323,6 +314,9 @@
if (hardCacheEnabled) { if (hardCacheEnabled) {
// Replace image with placeholder // Replace image with placeholder
img.src = '/static/images/placeholder.svg'; img.src = '/static/images/placeholder.svg';
img.onerror = function() {
handleImageError(img);
};
let id = img.getAttribute('data-id'); let id = img.getAttribute('data-id');
imageElements.push(img); imageElements.push(img);
@ -333,19 +327,82 @@
if (hardCacheEnabled) { if (hardCacheEnabled) {
checkImageStatus(); checkImageStatus();
} }
isFetching = false; // After appending new images, ensure the page is scrollable
ensureScrollable();
} else { } else {
// No more images to load // No more images to load
isFetching = false; noMoreImages = true;
// Optionally, remove the scroll event listener if no more pages
// window.removeEventListener('scroll', scrollHandler);
} }
isFetching = false;
}) })
.catch(error => { .catch(error => {
console.error('Error fetching next page:', error); console.error('Error fetching next page:', error);
isFetching = false; isFetching = false;
}); });
} }
/**
* Function to check image status via AJAX
*/
function checkImageStatus() {
if (!hardCacheEnabled) return;
if (imageIds.length === 0) {
// No images to check, do nothing
return;
}
// Send AJAX request to check image status
fetch(`/image_status?image_ids=${imageIds.join(',')}`)
.then(response => response.json())
.then(statusMap => {
imageElements = imageElements.filter(img => {
let id = img.getAttribute('data-id');
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);
});
}
// Initialize imageElements and imageIds
if (hardCacheEnabled) {
imageElements = Array.from(document.querySelectorAll('img[data-id]'));
imageIds = imageElements.map(img => img.getAttribute('data-id'));
// Replace images with placeholders
imageElements.forEach(img => {
img.src = '/static/images/placeholder.svg';
});
// 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 || noMoreImages) return;
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - scrollThreshold) {
// User scrolled near the bottom
fetchNextPage();
}
}); });
// Remove 'js-enabled' class from content // Remove 'js-enabled' class from content