code cleanup & fixed compatibility for non-JS users & fixed fullscreen images being in low resolution
This commit is contained in:
parent
0d083f53e7
commit
db89f9c781
13 changed files with 474 additions and 300 deletions
226
cache-images.go
226
cache-images.go
|
@ -25,14 +25,31 @@ import (
|
|||
var (
|
||||
cachingImages = make(map[string]*sync.Mutex)
|
||||
cachingImagesMu sync.Mutex
|
||||
cachingSemaphore = make(chan struct{}, 30) // Limit to concurrent downloads
|
||||
cachingSemaphore = make(chan struct{}, 100) // Limit to concurrent downloads
|
||||
|
||||
invalidImageIDs = make(map[string]struct{})
|
||||
invalidImageIDsMu sync.Mutex
|
||||
|
||||
imageURLMap = make(map[string]string) // mapping from imageID_type to imageURL
|
||||
imageURLMapMu sync.RWMutex // mutex for thread-safe access
|
||||
)
|
||||
|
||||
func cacheImage(imageURL, filename, imageID string) (string, bool, error) {
|
||||
func cacheImage(imageURL, imageID string, isThumbnail bool) (string, bool, error) {
|
||||
cacheDir := "image_cache"
|
||||
|
||||
if imageURL == "" {
|
||||
recordInvalidImageID(imageID)
|
||||
return "", false, fmt.Errorf("empty image URL for image ID %s", imageID)
|
||||
}
|
||||
|
||||
// Construct the filename based on the image ID and type
|
||||
var filename string
|
||||
if isThumbnail {
|
||||
filename = fmt.Sprintf("%s_thumb.webp", imageID)
|
||||
} else {
|
||||
filename = fmt.Sprintf("%s_full.webp", imageID)
|
||||
}
|
||||
|
||||
cachedImagePath := filepath.Join(cacheDir, filename)
|
||||
tempImagePath := cachedImagePath + ".tmp"
|
||||
|
||||
|
@ -181,46 +198,105 @@ func cacheImage(imageURL, filename, imageID string) (string, bool, error) {
|
|||
return cachedImagePath, true, nil
|
||||
}
|
||||
|
||||
func handleCachedImages(w http.ResponseWriter, r *http.Request) {
|
||||
func handleImageServe(w http.ResponseWriter, r *http.Request) {
|
||||
// Extract the image ID and type from the URL
|
||||
imageName := filepath.Base(r.URL.Path)
|
||||
idType := imageName
|
||||
|
||||
var imageID, imageType string
|
||||
|
||||
hasExtension := false
|
||||
if strings.HasSuffix(idType, ".webp") {
|
||||
// Cached image, remove extension
|
||||
idType = strings.TrimSuffix(idType, ".webp")
|
||||
hasExtension = true
|
||||
}
|
||||
|
||||
parts := strings.SplitN(idType, "_", 2)
|
||||
if len(parts) != 2 {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
imageID = parts[0]
|
||||
imageType = parts[1]
|
||||
|
||||
cacheDir := "image_cache"
|
||||
cachedImagePath := filepath.Join(cacheDir, imageName)
|
||||
|
||||
if _, err := os.Stat(cachedImagePath); os.IsNotExist(err) {
|
||||
printDebug("Cached image not found: %s, serving missing.svg", cachedImagePath)
|
||||
// Serve missing image
|
||||
missingImagePath := filepath.Join("static", "images", "missing.svg")
|
||||
w.Header().Set("Content-Type", "image/svg+xml")
|
||||
http.ServeFile(w, r, missingImagePath)
|
||||
return
|
||||
} else if err != nil {
|
||||
printWarn("Error checking image file: %v", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Determine the content type based on the file extension
|
||||
extension := strings.ToLower(filepath.Ext(cachedImagePath))
|
||||
var contentType string
|
||||
switch extension {
|
||||
case ".svg":
|
||||
contentType = "image/svg+xml"
|
||||
case ".jpg", ".jpeg":
|
||||
contentType = "image/jpeg"
|
||||
case ".png":
|
||||
contentType = "image/png"
|
||||
case ".gif":
|
||||
contentType = "image/gif"
|
||||
case ".webp":
|
||||
contentType = "image/webp"
|
||||
default:
|
||||
// Default to binary stream if unknown
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
filename := fmt.Sprintf("%s_%s.webp", imageID, imageType)
|
||||
cachedImagePath := filepath.Join(cacheDir, filename)
|
||||
|
||||
if hasExtension && imageType == "thumb" {
|
||||
// Requesting cached thumbnail image
|
||||
if _, err := os.Stat(cachedImagePath); err == nil {
|
||||
// Cached image exists, serve it
|
||||
contentType := "image/webp"
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000") // Cache the image for 1 year
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000")
|
||||
http.ServeFile(w, r, cachedImagePath)
|
||||
return
|
||||
} else {
|
||||
// Cached image not found
|
||||
if config.HardCacheEnabled {
|
||||
// Thumbnail should be cached, but not found
|
||||
serveMissingImage(w, r)
|
||||
return
|
||||
}
|
||||
// Else, proceed to proxy (if HardCacheEnabled is false)
|
||||
}
|
||||
}
|
||||
|
||||
// For full images, proceed to proxy the image
|
||||
|
||||
// Image not cached or caching not enabled
|
||||
imageKey := fmt.Sprintf("%s_%s", imageID, imageType)
|
||||
|
||||
imageURLMapMu.RLock()
|
||||
imageURL, exists := imageURLMap[imageKey]
|
||||
imageURLMapMu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
// Cannot find original URL, serve missing image
|
||||
serveMissingImage(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// For thumbnails, if HardCacheEnabled is true, and image not cached, serve missing image
|
||||
if imageType == "thumb" && config.HardCacheEnabled {
|
||||
// Thumbnail should be cached, but not found
|
||||
serveMissingImage(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// For full images, proceed to proxy the image
|
||||
|
||||
// Fetch the image from the original URL
|
||||
resp, err := http.Get(imageURL)
|
||||
if err != nil {
|
||||
printWarn("Error fetching image: %v", err)
|
||||
serveMissingImage(w, r)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check if the request was successful
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
serveMissingImage(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Set the Content-Type header to the type of the fetched image
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if contentType != "" && strings.HasPrefix(contentType, "image/") {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
} else {
|
||||
serveMissingImage(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Write the image content to the response
|
||||
if _, err := io.Copy(w, resp.Body); err != nil {
|
||||
printWarn("Error writing image to response: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func handleImageStatus(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -228,45 +304,54 @@ func handleImageStatus(w http.ResponseWriter, r *http.Request) {
|
|||
ids := strings.Split(imageIDs, ",")
|
||||
|
||||
statusMap := make(map[string]string)
|
||||
cacheDir := "image_cache"
|
||||
|
||||
printDebug("Received image status request for IDs: %v", ids)
|
||||
|
||||
invalidImageIDsMu.Lock()
|
||||
defer invalidImageIDsMu.Unlock()
|
||||
|
||||
for _, id := range ids {
|
||||
// Check if the image ID is in the invalidImageIDs map
|
||||
if _, invalid := invalidImageIDs[id]; invalid {
|
||||
// Image is invalid, set status to "missing"
|
||||
statusMap[id] = "/static/images/missing.svg"
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for different possible extensions
|
||||
extensions := []string{".webp", ".svg"}
|
||||
var cachedImagePath string
|
||||
var found bool
|
||||
// Check for cached full or thumbnail images
|
||||
cacheDir := "image_cache"
|
||||
extensions := []string{"webp", "svg"} // Extensions without leading dots
|
||||
imageReady := false
|
||||
|
||||
// Check thumbnail first
|
||||
for _, ext := range extensions {
|
||||
filename := id + ext
|
||||
path := filepath.Join(cacheDir, filename)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
cachedImagePath = "/image_cache/" + filename
|
||||
found = true
|
||||
thumbFilename := fmt.Sprintf("%s_thumb.%s", id, ext)
|
||||
thumbPath := filepath.Join(cacheDir, thumbFilename)
|
||||
|
||||
if _, err := os.Stat(thumbPath); err == nil {
|
||||
statusMap[id] = fmt.Sprintf("/image/%s_thumb.%s", id, ext)
|
||||
imageReady = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
statusMap[id] = cachedImagePath
|
||||
} else {
|
||||
// Image is not ready yet
|
||||
statusMap[id] = ""
|
||||
// If no thumbnail, check full image
|
||||
if !imageReady {
|
||||
for _, ext := range extensions {
|
||||
fullFilename := fmt.Sprintf("%s_full.%s", id, ext)
|
||||
fullPath := filepath.Join(cacheDir, fullFilename)
|
||||
|
||||
if _, err := os.Stat(fullPath); err == nil {
|
||||
statusMap[id] = fmt.Sprintf("/image/%s_full.%s", id, ext)
|
||||
imageReady = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printDebug("Status map: %v", statusMap)
|
||||
// If neither is ready
|
||||
if !imageReady {
|
||||
if !config.HardCacheEnabled {
|
||||
// Hard cache is disabled; use the proxy URL
|
||||
statusMap[id] = fmt.Sprintf("/image/%s_thumb", id)
|
||||
} else {
|
||||
// Hard cache is enabled; image is not yet cached
|
||||
// Do not set statusMap[id]; the frontend will keep checking
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(statusMap)
|
||||
|
@ -338,3 +423,20 @@ func removeImageResultFromCache(query string, page int, safe bool, lang string,
|
|||
delete(rc.results, keyStr)
|
||||
}
|
||||
}
|
||||
|
||||
func getContentType(ext string) string {
|
||||
switch strings.ToLower(ext) {
|
||||
case "svg":
|
||||
return "image/svg+xml"
|
||||
case "jpg", "jpeg":
|
||||
return "image/jpeg"
|
||||
case "png":
|
||||
return "image/png"
|
||||
case "gif":
|
||||
return "image/gif"
|
||||
case "webp":
|
||||
return "image/webp"
|
||||
default:
|
||||
return "application/octet-stream"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,26 +7,19 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
func handleImageProxy(w http.ResponseWriter, r *http.Request) {
|
||||
// Get the URL of the image from the query string
|
||||
imageURL := r.URL.Query().Get("url")
|
||||
if imageURL == "" {
|
||||
http.Error(w, "URL parameter is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
func serveImageProxy(w http.ResponseWriter, imageURL string) {
|
||||
// Fetch the image from the external URL
|
||||
resp, err := http.Get(imageURL)
|
||||
if err != nil {
|
||||
printWarn("Error fetching image: %v", err)
|
||||
serveMissingImage(w, r)
|
||||
serveMissingImage(w, nil)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check if the request was successful
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
serveMissingImage(w, r)
|
||||
serveMissingImage(w, nil)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -35,7 +28,7 @@ func handleImageProxy(w http.ResponseWriter, r *http.Request) {
|
|||
if contentType != "" && strings.HasPrefix(contentType, "image/") {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
} else {
|
||||
serveMissingImage(w, r)
|
||||
serveMissingImage(w, nil)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -39,27 +38,13 @@ func PerformBingImageSearch(query, safe, lang string, page int) ([]ImageSearchRe
|
|||
// Extract data using goquery
|
||||
var results []ImageSearchResult
|
||||
doc.Find(".iusc").Each(func(i int, s *goquery.Selection) {
|
||||
// Extract image source
|
||||
imgTag := s.Find("img")
|
||||
imgSrc, exists := imgTag.Attr("src")
|
||||
if !exists {
|
||||
imgSrc, exists = imgTag.Attr("data-src")
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Extract width and height if available
|
||||
width, _ := strconv.Atoi(imgTag.AttrOr("width", "0"))
|
||||
height, _ := strconv.Atoi(imgTag.AttrOr("height", "0"))
|
||||
|
||||
// Extract the m parameter (JSON-encoded image metadata)
|
||||
metadata, exists := s.Attr("m")
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the metadata to get the media URL and title
|
||||
// Parse the metadata to get the direct image URL and title
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(metadata), &data); err == nil {
|
||||
mediaURL, ok := data["murl"].(string)
|
||||
|
@ -67,19 +52,43 @@ func PerformBingImageSearch(query, safe, lang string, page int) ([]ImageSearchRe
|
|||
return
|
||||
}
|
||||
|
||||
imgURL, ok := data["imgurl"].(string)
|
||||
if !ok {
|
||||
imgURL = mediaURL // Fallback to mediaURL if imgurl is not available
|
||||
}
|
||||
|
||||
// Use imgURL as the direct image URL
|
||||
directImageURL := imgURL
|
||||
|
||||
// Extract title from the metadata
|
||||
title, _ := data["t"].(string)
|
||||
|
||||
// Apply the image proxy
|
||||
proxiedFullURL := "/imgproxy?url=" + mediaURL
|
||||
proxiedThumbURL := "/imgproxy?url=" + imgSrc
|
||||
// Extract dimensions if available
|
||||
width := 0
|
||||
height := 0
|
||||
if ow, ok := data["ow"].(float64); ok {
|
||||
width = int(ow)
|
||||
}
|
||||
if oh, ok := data["oh"].(float64); ok {
|
||||
height = int(oh)
|
||||
}
|
||||
|
||||
// Extract thumbnail URL from the 'turl' field
|
||||
thumbURL, _ := data["turl"].(string)
|
||||
if thumbURL == "" {
|
||||
// As a fallback, try to get it from the 'src' or 'data-src' attributes
|
||||
imgTag := s.Find("img")
|
||||
thumbURL, exists = imgTag.Attr("src")
|
||||
if !exists {
|
||||
thumbURL, _ = imgTag.Attr("data-src")
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, ImageSearchResult{
|
||||
Thumb: imgSrc,
|
||||
Thumb: thumbURL,
|
||||
Title: strings.TrimSpace(title),
|
||||
Full: imgSrc,
|
||||
Full: directImageURL,
|
||||
Source: mediaURL,
|
||||
ProxyFull: proxiedFullURL, // Proxied full-size image URL
|
||||
ProxyThumb: proxiedThumbURL, // Proxied thumbnail URL
|
||||
Width: width,
|
||||
Height: height,
|
||||
})
|
||||
|
|
|
@ -156,8 +156,6 @@ func PerformDeviantArtImageSearch(query, safe, lang string, page int) ([]ImageSe
|
|||
Width: 0,
|
||||
Height: 0,
|
||||
Source: resultURL,
|
||||
ProxyThumb: "/imgproxy?url=" + imgSrc, // Proxied thumbnail
|
||||
ProxyFull: "/imgproxy?url=" + imgSrc, // Proxied full-size image
|
||||
}
|
||||
}
|
||||
}(imgSrc, resultURL, title)
|
||||
|
|
|
@ -64,10 +64,6 @@ func PerformImgurImageSearch(query, safe, lang string, page int) ([]ImageSearchR
|
|||
width, _ := strconv.Atoi(s.Find("a img").AttrOr("width", "0"))
|
||||
height, _ := strconv.Atoi(s.Find("a img").AttrOr("height", "0"))
|
||||
|
||||
// Generate proxied URLs
|
||||
proxyFullURL := "/imgproxy?url=" + url.QueryEscape(imgSrc)
|
||||
proxyThumbURL := "/imgproxy?url=" + url.QueryEscape(thumbnailSrc)
|
||||
|
||||
results = append(results, ImageSearchResult{
|
||||
Thumb: thumbnailSrc,
|
||||
Title: strings.TrimSpace(title),
|
||||
|
@ -75,8 +71,6 @@ func PerformImgurImageSearch(query, safe, lang string, page int) ([]ImageSearchR
|
|||
Width: width,
|
||||
Height: height,
|
||||
Source: "https://imgur.com" + urlPath,
|
||||
ProxyFull: proxyFullURL,
|
||||
ProxyThumb: proxyThumbURL,
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -163,8 +163,6 @@ func PerformQwantImageSearch(query, safe, lang string, page int) ([]ImageSearchR
|
|||
Title: item.Title,
|
||||
Full: item.Media,
|
||||
Source: item.Url,
|
||||
ProxyFull: "/imgproxy?url=" + item.Media,
|
||||
ProxyThumb: "/imgproxy?url=" + item.Media,
|
||||
Width: item.Width,
|
||||
Height: item.Height,
|
||||
}
|
||||
|
|
139
images.go
139
images.go
|
@ -58,6 +58,12 @@ func handleImageSearch(w http.ResponseWriter, r *http.Request, settings UserSett
|
|||
"JsDisabled": jsDisabled,
|
||||
}
|
||||
|
||||
if r.URL.Query().Get("ajax") == "true" {
|
||||
// Render only the images
|
||||
renderTemplate(w, "images_only.html", data)
|
||||
return
|
||||
}
|
||||
|
||||
// Render the full page
|
||||
renderTemplate(w, "images.html", data)
|
||||
}
|
||||
|
@ -104,7 +110,6 @@ func getImageResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string
|
|||
func fetchImageResults(query, safe, lang string, page int, synchronous bool) []ImageSearchResult {
|
||||
var results []ImageSearchResult
|
||||
engineCount := len(imageSearchEngines)
|
||||
safeBool := safe == "active"
|
||||
|
||||
// Determine the engine to use based on the page number
|
||||
engineIndex := (page - 1) % engineCount
|
||||
|
@ -120,37 +125,47 @@ func fetchImageResults(query, safe, lang string, page int, synchronous bool) []I
|
|||
} else {
|
||||
for _, result := range searchResults {
|
||||
imageResult := result.(ImageSearchResult)
|
||||
if config.HardCacheEnabled {
|
||||
|
||||
// Skip image if thumbnail URL is empty
|
||||
if imageResult.Thumb == "" {
|
||||
printWarn("Skipping image with empty thumbnail URL. Full URL: %s", imageResult.Full)
|
||||
continue
|
||||
}
|
||||
|
||||
// Generate hash and set up caching
|
||||
hasher := md5.New()
|
||||
hasher.Write([]byte(imageResult.Full))
|
||||
hash := hex.EncodeToString(hasher.Sum(nil))
|
||||
filename := hash + ".webp"
|
||||
imageResult.ID = hash
|
||||
imageResult.ProxyFull = "/image_cache/" + filename
|
||||
|
||||
if synchronous {
|
||||
// Synchronously cache the image
|
||||
_, success, err := cacheImage(imageResult.Full, filename, imageResult.ID)
|
||||
// Store mapping from imageID_full and imageID_thumb to URLs
|
||||
imageURLMapMu.Lock()
|
||||
imageURLMap[fmt.Sprintf("%s_full", hash)] = imageResult.Full
|
||||
imageURLMap[fmt.Sprintf("%s_thumb", hash)] = imageResult.Thumb
|
||||
imageURLMapMu.Unlock()
|
||||
|
||||
// Set ProxyFull and ProxyThumb
|
||||
if config.HardCacheEnabled {
|
||||
// Cache the thumbnail image asynchronously
|
||||
go func(imgResult ImageSearchResult) {
|
||||
_, success, err := cacheImage(imgResult.Thumb, imgResult.ID, true)
|
||||
if err != nil || !success {
|
||||
printWarn("Failed to cache image %s: %v", imageResult.Full, err)
|
||||
// Fallback to proxy URL
|
||||
imageResult.ProxyFull = "/imgproxy?url=" + imageResult.Full
|
||||
printWarn("Failed to cache thumbnail image %s: %v", imgResult.Thumb, err)
|
||||
removeImageResultFromCache(query, page, safe == "active", lang, imgResult.ID)
|
||||
}
|
||||
}(imageResult)
|
||||
|
||||
// Set ProxyThumb to the proxy URL (initially placeholder)
|
||||
imageResult.ProxyThumb = fmt.Sprintf("/image/%s_thumb.webp", hash)
|
||||
|
||||
// Set ProxyFull to the proxy URL
|
||||
imageResult.ProxyFull = fmt.Sprintf("/image/%s_full", hash)
|
||||
} else {
|
||||
// Start caching and validation in the background
|
||||
go func(imgResult ImageSearchResult, originalURL, filename string) {
|
||||
_, success, err := cacheImage(originalURL, filename, imgResult.ID)
|
||||
if err != nil || !success {
|
||||
printWarn("Failed to cache image %s: %v", originalURL, err)
|
||||
removeImageResultFromCache(query, page, safeBool, lang, imgResult.ID)
|
||||
}
|
||||
}(imageResult, imageResult.Full, filename)
|
||||
}
|
||||
} else {
|
||||
// Use proxied URLs when hard cache is disabled
|
||||
imageResult.ProxyFull = "/imgproxy?url=" + imageResult.Full
|
||||
// Hard cache disabled, proxy both thumb and full images
|
||||
imageResult.ProxyThumb = fmt.Sprintf("/image/%s_thumb", hash)
|
||||
imageResult.ProxyFull = fmt.Sprintf("/image/%s_full", hash)
|
||||
}
|
||||
|
||||
results = append(results, imageResult)
|
||||
}
|
||||
}
|
||||
|
@ -170,44 +185,46 @@ func fetchImageResults(query, safe, lang string, page int, synchronous bool) []I
|
|||
}
|
||||
for _, result := range searchResults {
|
||||
imageResult := result.(ImageSearchResult)
|
||||
if config.HardCacheEnabled {
|
||||
|
||||
// Skip image if thumbnail URL is empty
|
||||
if imageResult.Thumb == "" {
|
||||
printWarn("Skipping image with empty thumbnail URL. Full URL: %s", imageResult.Full)
|
||||
continue
|
||||
}
|
||||
|
||||
// Generate hash and set up caching
|
||||
hasher := md5.New()
|
||||
hasher.Write([]byte(imageResult.Full))
|
||||
hash := hex.EncodeToString(hasher.Sum(nil))
|
||||
filename := hash + ".webp"
|
||||
imageResult.ID = hash
|
||||
imageResult.ProxyFull = "/image_cache/" + filename
|
||||
|
||||
if synchronous {
|
||||
// Synchronously cache the image
|
||||
_, success, err := cacheImage(imageResult.Full, filename, imageResult.ID)
|
||||
if err != nil {
|
||||
printWarn("Failed to cache image %s: %v", imageResult.Full, err)
|
||||
// Skip this image
|
||||
continue
|
||||
}
|
||||
if !success {
|
||||
// Skip this image
|
||||
continue
|
||||
// Store mapping from imageID_full and imageID_thumb to URLs
|
||||
imageURLMapMu.Lock()
|
||||
imageURLMap[fmt.Sprintf("%s_full", hash)] = imageResult.Full
|
||||
imageURLMap[fmt.Sprintf("%s_thumb", hash)] = imageResult.Thumb
|
||||
imageURLMapMu.Unlock()
|
||||
|
||||
if config.HardCacheEnabled {
|
||||
// Cache the thumbnail image asynchronously
|
||||
go func(imgResult ImageSearchResult) {
|
||||
_, success, err := cacheImage(imgResult.Thumb, imgResult.ID, true)
|
||||
if err != nil || !success {
|
||||
printWarn("Failed to cache thumbnail image %s: %v", imgResult.Thumb, err)
|
||||
removeImageResultFromCache(query, page, safe == "active", lang, imgResult.ID)
|
||||
}
|
||||
}(imageResult)
|
||||
|
||||
// Set ProxyThumb to the proxy URL (initially placeholder)
|
||||
imageResult.ProxyThumb = fmt.Sprintf("/image/%s_thumb.webp", hash)
|
||||
|
||||
// Set ProxyFull to the proxy URL
|
||||
imageResult.ProxyFull = fmt.Sprintf("/image/%s_full", hash)
|
||||
} else {
|
||||
// Start caching and validation in the background
|
||||
go func(imgResult ImageSearchResult, originalURL, filename string) {
|
||||
_, success, err := cacheImage(originalURL, filename, imgResult.ID)
|
||||
if err != nil {
|
||||
printWarn("Failed to cache image %s: %v", originalURL, err)
|
||||
}
|
||||
if !success {
|
||||
removeImageResultFromCache(query, page, safeBool, lang, imgResult.ID)
|
||||
}
|
||||
}(imageResult, imageResult.Full, filename)
|
||||
}
|
||||
} else {
|
||||
// Use proxied URLs when hard cache is disabled
|
||||
imageResult.ProxyThumb = "/imgproxy?url=" + imageResult.Thumb
|
||||
imageResult.ProxyFull = "/imgproxy?url=" + imageResult.Full
|
||||
// Hard cache disabled, proxy both thumb and full images
|
||||
imageResult.ProxyThumb = fmt.Sprintf("/image/%s_thumb", hash)
|
||||
imageResult.ProxyFull = fmt.Sprintf("/image/%s_full", hash)
|
||||
}
|
||||
|
||||
results = append(results, imageResult)
|
||||
}
|
||||
|
||||
|
@ -217,20 +234,20 @@ func fetchImageResults(query, safe, lang string, page int, synchronous bool) []I
|
|||
}
|
||||
}
|
||||
|
||||
// Filter out images that failed to cache or are invalid
|
||||
validResults := make([]ImageSearchResult, 0, len(results))
|
||||
for _, imageResult := range results {
|
||||
if imageResult.ProxyFull != "" {
|
||||
validResults = append(validResults, imageResult)
|
||||
} else {
|
||||
printWarn("Skipping invalid image with ID %s", imageResult.ID)
|
||||
}
|
||||
}
|
||||
// // Filter out images that failed to cache or are invalid
|
||||
// validResults := make([]ImageSearchResult, 0, len(results))
|
||||
// for _, imageResult := range results {
|
||||
// if imageResult.ProxyFull != "" {
|
||||
// validResults = append(validResults, imageResult)
|
||||
// } else {
|
||||
// printWarn("Skipping invalid image with ID %s", imageResult.ID)
|
||||
// }
|
||||
// }
|
||||
|
||||
// Final debug print to show the count of results fetched
|
||||
printInfo("Fetched %d image results for overall page %d", len(results), page)
|
||||
|
||||
return validResults
|
||||
return results
|
||||
}
|
||||
|
||||
func wrapImageSearchFunc(f func(string, string, string, int) ([]ImageSearchResult, time.Duration, error)) func(string, string, string, int) ([]SearchResult, time.Duration, error) {
|
||||
|
|
5
main.go
5
main.go
|
@ -214,11 +214,12 @@ func runServer() {
|
|||
http.HandleFunc("/", handleSearch)
|
||||
http.HandleFunc("/search", handleSearch)
|
||||
http.HandleFunc("/suggestions", handleSuggestions)
|
||||
http.HandleFunc("/imgproxy", handleImageProxy)
|
||||
// The /imgproxy handler is deprecated, now its handled by /image/
|
||||
// http.HandleFunc("/imgproxy", handleImageProxy)
|
||||
http.HandleFunc("/node", handleNodeRequest)
|
||||
http.HandleFunc("/settings", handleSettings)
|
||||
http.HandleFunc("/save-settings", handleSaveSettings)
|
||||
http.HandleFunc("/image_cache/", handleCachedImages)
|
||||
http.HandleFunc("/image/", handleImageServe)
|
||||
http.HandleFunc("/image_status", handleImageStatus)
|
||||
http.HandleFunc("/opensearch.xml", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/opensearchdescription+xml")
|
||||
|
|
|
@ -44,8 +44,9 @@ body, html {
|
|||
color: var(--text-color);
|
||||
}
|
||||
|
||||
body {
|
||||
visibility: hidden;
|
||||
button, p {
|
||||
font-family: 'Inter', Arial, Helvetica, sans-serif;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
body.menu-open {
|
||||
|
@ -259,6 +260,7 @@ body.menu-open {
|
|||
}
|
||||
|
||||
.settings-icon-link-search:hover {
|
||||
text-decoration: none;
|
||||
color: var(--blue);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
|
|
@ -1,21 +1,28 @@
|
|||
document.addEventListener('DOMContentLoaded', function() {
|
||||
let viewerOpen = false;
|
||||
let currentIndex = -1;
|
||||
let imageList = [];
|
||||
|
||||
// Initialize imageList with all images on the page
|
||||
function initializeImageList() {
|
||||
imageList = Array.from(document.querySelectorAll('.image img.clickable'));
|
||||
}
|
||||
|
||||
const viewerOverlay = document.getElementById('image-viewer-overlay');
|
||||
viewerOverlay.innerHTML = `
|
||||
<div id="image-viewer" class="image_view image_hide">
|
||||
<div class="image-view-close">
|
||||
<!-- <button class="btn-nostyle">
|
||||
<div id="viewer-prev-button" class="material-icons-round icon_visibility clickable image-before">navigate_before</div>
|
||||
<button class="btn-nostyle" id="viewer-prev-button">
|
||||
<div class="material-icons-round icon_visibility clickable image-before">navigate_before</div>
|
||||
</button>
|
||||
<button class="btn-nostyle">
|
||||
<div id="viewer-next-button" class="material-icons-round icon_visibility clickable image-next">navigate_next</div>
|
||||
</button> FIX THIS LATER! --!>
|
||||
<button class="btn-nostyle">
|
||||
<div id="viewer-close-button" class="material-icons-round icon_visibility clickable image-close">close</div>
|
||||
<button class="btn-nostyle" id="viewer-next-button">
|
||||
<div class="material-icons-round icon_visibility clickable image-next">navigate_next</div>
|
||||
</button>
|
||||
<button class="btn-nostyle" id="viewer-close-button">
|
||||
<div class="material-icons-round icon_visibility clickable image-close">close</div>
|
||||
</button>
|
||||
</div>
|
||||
<a class="image-viewer-link">
|
||||
<a class="image-viewer-link" id="viewer-image-link" target="_blank">
|
||||
<div class="view-image" id="viewer-image-container">
|
||||
<img id="viewer-image" class="view-image-img" src="" alt="">
|
||||
</div>
|
||||
|
@ -23,8 +30,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
<p class="image-alt" id="viewer-title"></p>
|
||||
<p>View source: <a id="viewer-source-button" class="image-source" href="" target="_blank"></a></p>
|
||||
<p>
|
||||
<a class="full-size" href="#">Show source website</a>
|
||||
<a class="proxy-size" href="#">Show in fullscreen</a>
|
||||
<a class="full-size" id="viewer-full-size-link" href="#" target="_blank">Show source website</a>
|
||||
<a class="proxy-size" id="viewer-proxy-size-link" href="#" target="_blank">Show in fullscreen</a>
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
@ -32,36 +39,45 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
const imageView = document.getElementById('image-viewer');
|
||||
|
||||
function openImageViewer(element) {
|
||||
initializeImageList(); // Update the image list
|
||||
|
||||
const parentImageDiv = element.closest('.image');
|
||||
if (!parentImageDiv) return;
|
||||
|
||||
const imgElement = parentImageDiv.querySelector('img.clickable');
|
||||
const fullImageUrl = imgElement.dataset.proxyFull; // Use data-proxy-full for ProxyFull
|
||||
const thumbnailUrl = imgElement.src; // Use ProxyThumb for the thumbnail
|
||||
const title = imgElement.alt;
|
||||
const sourceUrl = parentImageDiv.querySelector('.img_source').href; // Source webpage URL
|
||||
currentIndex = imageList.findIndex(img => img === parentImageDiv.querySelector('img.clickable'));
|
||||
if (currentIndex === -1) return;
|
||||
|
||||
if (!fullImageUrl || viewerOpen) {
|
||||
return; // Don't open if data is missing or viewer is already open
|
||||
}
|
||||
displayImage(currentIndex);
|
||||
viewerOpen = true;
|
||||
|
||||
viewerOverlay.style.display = 'flex';
|
||||
imageView.classList.remove('image_hide');
|
||||
imageView.classList.add('image_show');
|
||||
}
|
||||
|
||||
function displayImage(index) {
|
||||
if (index < 0 || index >= imageList.length) return;
|
||||
|
||||
const imgElement = imageList[index];
|
||||
const parentImageDiv = imgElement.closest('.image');
|
||||
const fullImageUrl = imgElement.getAttribute('data-full'); // Use data-full for the full image URL
|
||||
const title = imgElement.alt || '';
|
||||
const sourceUrl = parentImageDiv.querySelector('.img_source').href || '#'; // Source webpage URL
|
||||
|
||||
const viewerImage = document.getElementById('viewer-image');
|
||||
const viewerTitle = document.getElementById('viewer-title');
|
||||
const viewerSourceButton = document.getElementById('viewer-source-button');
|
||||
const fullSizeLink = imageView.querySelector('.full-size');
|
||||
const proxySizeLink = imageView.querySelector('.proxy-size');
|
||||
const fullSizeLink = document.getElementById('viewer-full-size-link');
|
||||
const proxySizeLink = document.getElementById('viewer-proxy-size-link');
|
||||
const viewerImageLink = document.getElementById('viewer-image-link');
|
||||
|
||||
// Set the viewer image to ProxyFull
|
||||
// Set the viewer image to the full image URL
|
||||
viewerImage.src = fullImageUrl;
|
||||
viewerTitle.textContent = title;
|
||||
viewerSourceButton.href = sourceUrl;
|
||||
fullSizeLink.href = sourceUrl; // Link to the source website
|
||||
proxySizeLink.href = fullImageUrl; // Link to the proxied full-size image
|
||||
|
||||
viewerOverlay.style.display = 'flex';
|
||||
imageView.classList.remove('image_hide');
|
||||
imageView.classList.add('image_show');
|
||||
viewerImageLink.href = fullImageUrl; // Make image clickable to open in new tab
|
||||
}
|
||||
|
||||
// Attach event listener to the document body
|
||||
|
@ -80,10 +96,30 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
imageView.classList.add('image_hide');
|
||||
viewerOverlay.style.display = 'none';
|
||||
viewerOpen = false;
|
||||
currentIndex = -1;
|
||||
}
|
||||
|
||||
// Close viewer on overlay or button click
|
||||
// Navigation functions
|
||||
function showPreviousImage() {
|
||||
if (currentIndex > 0) {
|
||||
currentIndex--;
|
||||
displayImage(currentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function showNextImage() {
|
||||
if (currentIndex < imageList.length - 1) {
|
||||
currentIndex++;
|
||||
displayImage(currentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners for navigation and closing
|
||||
document.getElementById('viewer-close-button').addEventListener('click', closeImageViewer);
|
||||
document.getElementById('viewer-prev-button').addEventListener('click', showPreviousImage);
|
||||
document.getElementById('viewer-next-button').addEventListener('click', showNextImage);
|
||||
|
||||
// Close viewer when clicking outside the image
|
||||
viewerOverlay.addEventListener('click', function(e) {
|
||||
if (e.target === viewerOverlay) {
|
||||
closeImageViewer();
|
||||
|
@ -95,6 +131,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
if (viewerOverlay.style.display === 'flex') {
|
||||
if (e.key === 'Escape') {
|
||||
closeImageViewer();
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
showPreviousImage();
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
showNextImage();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -153,9 +153,9 @@
|
|||
<button name="t" value="file" class="clickable">{{ translate "torrents" }}</button>
|
||||
</div>
|
||||
</div>
|
||||
{{ if not .JsDisabled }}
|
||||
<noscript>
|
||||
<input type="hidden" name="js_enabled" value="true">
|
||||
{{ end }}
|
||||
</noscript>
|
||||
</form>
|
||||
<form class="results_settings" action="/search" method="get">
|
||||
<input type="hidden" name="q" value="{{ .Query }}">
|
||||
|
@ -179,11 +179,13 @@
|
|||
{{ range $index, $result := .Results }}
|
||||
<div class="image">
|
||||
{{ if $.HardCacheEnabled }}
|
||||
{{ if $.JsDisabled }}
|
||||
<noscript>
|
||||
<!-- JavaScript is disabled; serve actual images -->
|
||||
<img src="{{ $result.ProxyFull }}" alt="{{ $result.Title }}" class="clickable" />
|
||||
{{ else }}
|
||||
</noscript>
|
||||
|
||||
<!-- JavaScript is enabled; use placeholders -->
|
||||
<div id="content" class="js-enabled">
|
||||
<img
|
||||
src="/static/images/placeholder.svg"
|
||||
data-id="{{ $result.ID }}"
|
||||
|
@ -192,7 +194,7 @@
|
|||
alt="{{ $result.Title }}"
|
||||
class="clickable"
|
||||
/>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ else }}
|
||||
<!-- HardCacheEnabled is false; serve images directly -->
|
||||
<img src="{{ $result.ProxyFull }}" alt="{{ $result.Title }}" class="clickable" />
|
||||
|
@ -320,10 +322,12 @@
|
|||
};
|
||||
|
||||
let id = img.getAttribute('data-id');
|
||||
if (id) { // Only include if ID is not empty
|
||||
imageElements.push(img);
|
||||
imageIds.push(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (hardCacheEnabled) {
|
||||
checkImageStatus();
|
||||
|
@ -381,7 +385,9 @@
|
|||
// Initialize imageElements and imageIds
|
||||
if (hardCacheEnabled) {
|
||||
imageElements = Array.from(document.querySelectorAll('img[data-id]'));
|
||||
imageIds = imageElements.map(img => img.getAttribute('data-id'));
|
||||
imageIds = imageElements
|
||||
.map(img => img.getAttribute('data-id'))
|
||||
.filter(id => id); // Exclude empty IDs
|
||||
|
||||
// Replace images with placeholders
|
||||
imageElements.forEach(img => {
|
||||
|
@ -410,19 +416,5 @@
|
|||
document.getElementById('content').classList.remove('js-enabled');
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
// Check if 'js_enabled' is not present in the URL
|
||||
if (!window.location.search.includes('js_enabled=true')) {
|
||||
// Redirect to the same URL with 'js_enabled=true'
|
||||
var separator = window.location.search.length ? '&' : '?';
|
||||
window.location.href = window.location.href + separator + 'js_enabled=true';
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Check if JavaScript is enabled and modify the DOM accordingly
|
||||
document.getElementById('content').classList.remove('js-enabled');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,4 +1,3 @@
|
|||
<!-- Images Grid -->
|
||||
{{ range $index, $result := .Results }}
|
||||
<div class="image">
|
||||
<img
|
||||
|
|
|
@ -7,6 +7,20 @@
|
|||
<meta name="darkreader-lock">
|
||||
{{ end }}
|
||||
<title>{{ translate "site_description" }}</title>
|
||||
<!-- Inline Style to Avoid Flashbang -->
|
||||
<style>
|
||||
body {
|
||||
background-color: {{ if .IsThemeDark }} #121212 {{ else }} #ffffff {{ end }};
|
||||
color: {{ if .IsThemeDark }} #ffffff {{ else }} #000000 {{ end }};
|
||||
}
|
||||
#js-enabled {
|
||||
display: none;
|
||||
}
|
||||
#js-disabled {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<link rel="stylesheet" href="/static/css/style-search.css">
|
||||
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
||||
<link rel="search" type="application/opensearchdescription+xml" title="{{ translate "site_name" }}" href="/opensearch.xml">
|
||||
|
@ -17,9 +31,14 @@
|
|||
<link rel="apple-touch-icon" href="{{ .IconPathPNG }}">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Menu Button -->
|
||||
<button class="material-icons-round settings-icon-link-search" onclick="openNav()">menu</button>
|
||||
|
||||
<!-- Menu Button -->
|
||||
<div id="js-enabled">
|
||||
<button class="material-icons-round settings-icon-link-search" onclick="openNav()">menu</button>
|
||||
</div>
|
||||
<div id="js-disabled">
|
||||
<a href="/settings" class="material-icons-round settings-icon-link-search">menu</a>
|
||||
</div>
|
||||
|
||||
<!-- Side Navigation Menu -->
|
||||
<div id="mySidenav" class="side-nav">
|
||||
|
@ -140,36 +159,46 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="search-type-icons">
|
||||
<input type="hidden" name="p" value="1">
|
||||
|
||||
<div class="icon-button" onclick="document.getElementById('sub-search-wrapper-ico-text').click()">
|
||||
<button id="sub-search-wrapper-ico-text" class="material-icons-round clickable" name="t" value="text">search</button>
|
||||
<div class="icon-button">
|
||||
<button id="sub-search-wrapper-ico-text" class="material-icons-round clickable" name="t" value="text">
|
||||
<span>search</span>
|
||||
<p>{{ translate "web" }}</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="icon-button" onclick="document.getElementById('sub-search-wrapper-ico-image').click()">
|
||||
<button id="sub-search-wrapper-ico-image" class="material-icons-round clickable" name="t" value="image">image</button>
|
||||
<div class="icon-button">
|
||||
<button id="sub-search-wrapper-ico-image" class="material-icons-round clickable" name="t" value="image">
|
||||
<span>image</span>
|
||||
<p>{{ translate "images" }}</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="icon-button" onclick="document.getElementById('sub-search-wrapper-ico-video').click()">
|
||||
<button id="sub-search-wrapper-ico-video" class="material-icons-round clickable" name="t" value="video">movie</button>
|
||||
<div class="icon-button">
|
||||
<button id="sub-search-wrapper-ico-video" class="material-icons-round clickable" name="t" value="video">
|
||||
<span>movie</span>
|
||||
<p>{{ translate "videos" }}</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="icon-button" onclick="document.getElementById('sub-search-wrapper-ico-forum').click()">
|
||||
<button id="sub-search-wrapper-ico-forum" class="material-icons-round clickable" name="t" value="forum">forum</button>
|
||||
<div class="icon-button">
|
||||
<button id="sub-search-wrapper-ico-forum" class="material-icons-round clickable" name="t" value="forum">
|
||||
<span>forum</span>
|
||||
<p>{{ translate "forums" }}</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="icon-button" onclick="document.getElementById('sub-search-wrapper-ico-map').click()">
|
||||
<button id="sub-search-wrapper-ico-map" class="material-icons-round clickable" name="t" value="map">map</button>
|
||||
<div class="icon-button">
|
||||
<button id="sub-search-wrapper-ico-map" class="material-icons-round clickable" name="t" value="map">
|
||||
<span>map</span>
|
||||
<p>{{ translate "maps" }}</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="icon-button" onclick="document.getElementById('sub-search-wrapper-ico-file').click()">
|
||||
<button id="sub-search-wrapper-ico-file" class="material-icons-round clickable" name="t" value="file">share</button>
|
||||
<div class="icon-button">
|
||||
<button id="sub-search-wrapper-ico-file" class="material-icons-round clickable" name="t" value="file">
|
||||
<span>share</span>
|
||||
<p>{{ translate "torrents" }}</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -180,9 +209,9 @@
|
|||
<script defer src="/static/js/sidemenu.js"></script>
|
||||
<script defer src="/static/js/autocomplete.js"></script>
|
||||
<script>
|
||||
window.addEventListener('load', function() {
|
||||
document.body.style.visibility = 'visible';
|
||||
});
|
||||
// When JS is detected, update the DOM
|
||||
document.getElementById('js-enabled').style.display = 'block';
|
||||
document.getElementById('js-disabled').style.display = 'none';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Add table
Reference in a new issue