diff --git a/cache-images.go b/cache-images.go index 5c9eec8..46a2f47 100644 --- a/cache-images.go +++ b/cache-images.go @@ -116,7 +116,7 @@ func cacheImage(imageURL, filename string) (string, error) { } if err != nil { - return "", err + return "", fmt.Errorf("failed to decode image: %v", err) } // Ensure the cache directory exists @@ -206,12 +206,23 @@ func handleImageStatus(w http.ResponseWriter, r *http.Request) { printDebug("Status map: %v", statusMap) for _, id := range ids { - filename := id + ".webp" - cachedImagePath := filepath.Join(cacheDir, filename) + // Check for different possible extensions + extensions := []string{".webp", ".svg"} + var cachedImagePath string + var found bool - if _, err := os.Stat(cachedImagePath); err == nil { - // Image is cached and ready - statusMap[id] = "/image_cache/" + filename + 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 + break + } + } + + if found { + statusMap[id] = cachedImagePath } else { // Image is not ready statusMap[id] = "" diff --git a/cache.go b/cache.go index a885745..a5058b9 100644 --- a/cache.go +++ b/cache.go @@ -26,13 +26,14 @@ type TextSearchResult struct { type ImageSearchResult struct { ID string - Thumbnail string Title string - Media string + Full string // Full-size image URL + Thumb string // Thumbnail image URL + ProxyFull string // Proxied full-size image URL + ProxyThumb string // Proxied thumbnail image URL (from cache) + Source string // Source webpage URL Width int Height int - Source string - ThumbProxy string } type VideoResult struct { diff --git a/common.go b/common.go index b128041..6b645c2 100755 --- a/common.go +++ b/common.go @@ -7,6 +7,7 @@ import ( "html/template" "net/http" "strings" + "time" ) var ( @@ -26,8 +27,14 @@ var ( return string(jsonBytes), nil }, } + searchEngines []SearchEngine ) +type SearchEngine struct { + Name string + Func func(string, string, string, int) ([]SearchResult, time.Duration, error) +} + // Helper function to render templates without elapsed time measurement func renderTemplate(w http.ResponseWriter, tmplName string, data map[string]interface{}) { // Parse the template with common functions (including translate) diff --git a/images-bing.go b/images-bing.go index 48ba32d..eaa1c4d 100644 --- a/images-bing.go +++ b/images-bing.go @@ -68,13 +68,15 @@ func PerformBingImageSearch(query, safe, lang string, page int) ([]ImageSearchRe mediaURL, ok := data["murl"].(string) if ok { // Apply the image proxy - proxiedURL := "/imgproxy?url=" + mediaURL + proxiedFullURL := "/imgproxy?url=" + imgSrc + proxiedThumbURL := "/imgproxy?url=" + mediaURL results = append(results, ImageSearchResult{ - Thumbnail: imgSrc, + Thumb: imgSrc, Title: strings.TrimSpace(title), - Media: mediaURL, + Full: imgSrc, Source: mediaURL, - ThumbProxy: proxiedURL, // Use the proxied URL + ProxyFull: proxiedFullURL, // Proxied full-size image URL + ProxyThumb: proxiedThumbURL, // Proxied thumbnail URL Width: width, Height: height, }) diff --git a/images-deviantart.go b/images-deviantart.go index 3b12def..e408616 100644 --- a/images-deviantart.go +++ b/images-deviantart.go @@ -152,11 +152,12 @@ func PerformDeviantArtImageSearch(query, safe, lang string, page int) ([]ImageSe if isValidImageURL(imgSrc, DeviantArtImageUserAgent, resultURL) { resultsChan <- ImageSearchResult{ Title: strings.TrimSpace(title), - Media: imgSrc, + Full: imgSrc, Width: 0, Height: 0, Source: resultURL, - ThumbProxy: "/imgproxy?url=" + imgSrc, + ProxyThumb: "/imgproxy?url=" + imgSrc, // Proxied thumbnail + ProxyFull: "/imgproxy?url=" + imgSrc, // Proxied full-size image } } }(imgSrc, resultURL, title) diff --git a/images-imgur.go b/images-imgur.go index ede8d10..826b48e 100644 --- a/images-imgur.go +++ b/images-imgur.go @@ -64,14 +64,19 @@ 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{ - Thumbnail: thumbnailSrc, + Thumb: thumbnailSrc, Title: strings.TrimSpace(title), - Media: imgSrc, + Full: imgSrc, Width: width, Height: height, Source: "https://imgur.com" + urlPath, - ThumbProxy: imgSrc, //"/img_proxy?url=" + url.QueryEscape(imgSrc) + ProxyFull: proxyFullURL, + ProxyThumb: proxyThumbURL, }) }) diff --git a/images-quant.go b/images-quant.go index 8690239..67a7ae1 100644 --- a/images-quant.go +++ b/images-quant.go @@ -14,12 +14,12 @@ type QwantAPIResponse struct { Data struct { Result struct { Items []struct { - Media string `json:"media"` - Thumbnail string `json:"thumbnail"` - Title string `json:"title"` - Url string `json:"url"` - Width int `json:"width"` - Height int `json:"height"` + Media string `json:"media"` + //Thumbnail string `json:"thumbnail"` + Title string `json:"title"` + Url string `json:"url"` + Width int `json:"width"` + Height int `json:"height"` } `json:"items"` } `json:"result"` } `json:"data"` @@ -125,7 +125,7 @@ func PerformQwantImageSearch(query, safe, lang string, page int) ([]ImageSearchR return nil, 0, err } - req.Header.Set("User-Agent", ImageUserAgent) + req.Header.Set("User-Agent", ImageUserAgent) // Quant seems to not like some specific User-Agent strings resp, err := client.Do(req) if err != nil { @@ -148,22 +148,23 @@ func PerformQwantImageSearch(query, safe, lang string, page int) ([]ImageSearchR for i, item := range apiResp.Data.Result.Items { wg.Add(1) go func(i int, item struct { - Media string `json:"media"` - Thumbnail string `json:"thumbnail"` - Title string `json:"title"` - Url string `json:"url"` - Width int `json:"width"` - Height int `json:"height"` + Media string `json:"media"` + //Thumbnail string `json:"thumbnail"` + Title string `json:"title"` + Url string `json:"url"` + Width int `json:"width"` + Height int `json:"height"` }) { defer wg.Done() // Populate the result results[i] = ImageSearchResult{ - Thumbnail: item.Thumbnail, + Thumb: item.Media, // item.Thumbnail is not working Title: item.Title, - Media: item.Media, + Full: item.Media, Source: item.Url, - ThumbProxy: "/imgproxy?url=" + item.Media, + ProxyFull: "/imgproxy?url=" + item.Media, + ProxyThumb: "/imgproxy?url=" + item.Media, Width: item.Width, Height: item.Height, } diff --git a/images.go b/images.go index 54126ab..eb9bc35 100755 --- a/images.go +++ b/images.go @@ -12,9 +12,9 @@ var imageSearchEngines []SearchEngine func init() { imageSearchEngines = []SearchEngine{ - {Name: "Qwant", Func: wrapImageSearchFunc(PerformQwantImageSearch), Weight: 1}, - {Name: "Bing", Func: wrapImageSearchFunc(PerformBingImageSearch), Weight: 2}, - {Name: "DeviantArt", Func: wrapImageSearchFunc(PerformDeviantArtImageSearch), Weight: 3}, + {Name: "Qwant", Func: wrapImageSearchFunc(PerformQwantImageSearch)}, + {Name: "Bing", Func: wrapImageSearchFunc(PerformBingImageSearch)}, + {Name: "DeviantArt", Func: wrapImageSearchFunc(PerformDeviantArtImageSearch)}, //{Name: "Imgur", Func: wrapImageSearchFunc(PerformImgurImageSearch), Weight: 4}, // Image proxy not working } } @@ -90,8 +90,7 @@ func fetchImageResults(query, safe, lang string, page int) []ImageSearchResult { for _, engine := range imageSearchEngines { printInfo("Using image search engine: %s", engine.Name) - searchResults, duration, err := engine.Func(query, safe, lang, page) - updateEngineMetrics(&engine, duration, err == nil) + searchResults, _, err := engine.Func(query, safe, lang, page) if err != nil { printWarn("Error performing image search with %s: %v", engine.Name, err) continue @@ -100,19 +99,15 @@ func fetchImageResults(query, safe, lang string, page int) []ImageSearchResult { for _, result := range searchResults { imageResult := result.(ImageSearchResult) if config.HardCacheDuration > 0 { - // Save the original Media URL before overwriting - originalMediaURL := imageResult.Media - - // Generate hash from the original media URL + // Generate hash from the original full-size image URL hasher := md5.New() - hasher.Write([]byte(originalMediaURL)) + hasher.Write([]byte(imageResult.Full)) hash := hex.EncodeToString(hasher.Sum(nil)) filename := hash + ".webp" - // Set the Media URL to point to the cached image path + // Set the Full URL to point to the cached image path cacheURL := "/image_cache/" + filename - imageResult.Media = cacheURL - imageResult.ThumbProxy = cacheURL + imageResult.ProxyFull = cacheURL // Assign the ID imageResult.ID = hash @@ -123,7 +118,7 @@ func fetchImageResults(query, safe, lang string, page int) []ImageSearchResult { if err != nil { printWarn("Failed to cache image %s: %v", originalURL, err) } - }(originalMediaURL, filename) + }(imageResult.Full, filename) } results = append(results, imageResult) } diff --git a/init.go b/init.go index 9c59e31..43d6d56 100644 --- a/init.go +++ b/init.go @@ -15,6 +15,7 @@ type Config struct { WebsiteEnabled bool LogLevel int HardCacheDuration time.Duration + HardCacheEnabled bool } var defaultConfig = Config{ @@ -27,6 +28,7 @@ var defaultConfig = Config{ WebsiteEnabled: true, LogLevel: 1, HardCacheDuration: 0, + HardCacheEnabled: false, } const configFilePath = "config.ini" @@ -62,6 +64,10 @@ func main() { startElection() } + if config.HardCacheDuration > 0 { + config.HardCacheEnabled = true + } + go startNodeClient() runServer() diff --git a/run.bat b/run.bat index 2709b17..f0baf09 100755 --- a/run.bat +++ b/run.bat @@ -5,7 +5,7 @@ rem Directory where the Go files are located set GO_DIR=C:\path\to\your\go\files rem Explicitly list the main files in the required order -set FILES=main.go init.go search-engine.go text.go text-google.go text-librex.go text-brave.go text-duckduckgo.go common.go cache.go agent.go files.go files-thepiratebay.go files-torrentgalaxy.go forums.go get-searchxng.go imageproxy.go images.go images-imgur.go images-quant.go map.go node.go open-search.go video.go +set FILES=main.go init.go text.go text-google.go text-librex.go text-brave.go text-duckduckgo.go common.go cache.go agent.go files.go files-thepiratebay.go files-torrentgalaxy.go forums.go get-searchxng.go imageproxy.go images.go images-imgur.go images-quant.go map.go node.go open-search.go video.go rem Change to the directory with the Go files pushd %GO_DIR% diff --git a/run.sh b/run.sh index 6aa8efa..bf384d9 100755 --- a/run.sh +++ b/run.sh @@ -4,7 +4,6 @@ FILES=" ./main.go ./init.go -./search-engine.go ./text.go ./text-google.go ./text-librex.go diff --git a/search-engine.go b/search-engine.go deleted file mode 100644 index 1396b4e..0000000 --- a/search-engine.go +++ /dev/null @@ -1,151 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "log" - "math/rand" - "net/http" - "sync" - "time" -) - -var ( - searchEngineLock sync.Mutex - searchEngines []SearchEngine // Ensure this variable is defined -) - -// SearchEngine struct now includes metrics for calculating reputation. -type SearchEngine struct { - Name string - Func func(string, string, string, int) ([]SearchResult, time.Duration, error) - Weight int - TotalRequests int - TotalTime time.Duration - SuccessfulSearches int - FailedSearches int - IsCrawler bool // Indicates if this search engine is a crawler - Host string // Host of the crawler - Port int // Port of the crawler - AuthCode string // Auth code for the crawler -} - -// init function seeds the random number generator. -func init() { - rand.Seed(time.Now().UnixNano()) - // Initialize the searchEngines list - searchEngines = []SearchEngine{ - {Name: "Google", Func: wrapTextSearchFunc(PerformGoogleTextSearch), Weight: 1}, - {Name: "LibreX", Func: wrapTextSearchFunc(PerformLibreXTextSearch), Weight: 2}, - {Name: "Brave", Func: wrapTextSearchFunc(PerformBraveTextSearch), Weight: 2}, - {Name: "DuckDuckGo", Func: wrapTextSearchFunc(PerformDuckDuckGoTextSearch), Weight: 5}, - // {Name: "SearXNG", Func: wrapTextSearchFunc(PerformSearXNGTextSearch), Weight: 2}, // Uncomment when implemented - } -} - -// Selects a search engine based on weighted random selection with dynamic weighting. -func selectSearchEngine(engines []SearchEngine) SearchEngine { - searchEngineLock.Lock() - defer searchEngineLock.Unlock() - - // Recalculate weights based on average response time and success rate. - for i := range engines { - engines[i].Weight = calculateReputation(engines[i]) - } - - totalWeight := 0 - for _, engine := range engines { - totalWeight += engine.Weight - } - - randValue := rand.Intn(totalWeight) - for _, engine := range engines { - if randValue < engine.Weight { - return engine - } - randValue -= engine.Weight - } - - return engines[0] // fallback to the first engine -} - -// Updates the engine's performance metrics. -func updateEngineMetrics(engine *SearchEngine, responseTime time.Duration, success bool) { - searchEngineLock.Lock() - defer searchEngineLock.Unlock() - - engine.TotalRequests++ - engine.TotalTime += responseTime - if success { - engine.SuccessfulSearches++ - } else { - engine.FailedSearches++ - } - engine.Weight = calculateReputation(*engine) -} - -// Calculates the reputation of the search engine based on average response time and success rate. -func calculateReputation(engine SearchEngine) int { - const referenceTime = time.Second // 1 second reference time in nanoseconds (1000 ms) - - if engine.TotalRequests == 0 { - return 10 // Default weight for new engines - } - - // Calculate average response time in seconds. - avgResponseTime := engine.TotalTime.Seconds() / float64(engine.TotalRequests) - - // Calculate success rate. - successRate := float64(engine.SuccessfulSearches) / float64(engine.TotalRequests) - - // Combine response time and success rate into a single reputation score. - // The formula can be adjusted to weigh response time and success rate differently. - reputation := (referenceTime.Seconds() / avgResponseTime) * successRate - - // Scale reputation for better interpretability (e.g., multiply by 10) - return int(reputation * 10) -} - -func fetchSearchResults(query, safe, lang, searchType string, page int) []SearchResult { - var results []SearchResult - - engine := selectSearchEngine(searchEngines) - log.Printf("Using search engine: %s", engine.Name) - - if engine.IsCrawler { - searchResults, duration, err := fetchSearchFromCrawler(engine, query, safe, lang, searchType, page) - updateEngineMetrics(&engine, duration, err == nil) - if err != nil { - log.Printf("Error performing search with crawler %s: %v", engine.Name, err) - return nil - } - results = append(results, searchResults...) - } else { - searchResults, duration, err := engine.Func(query, safe, lang, page) - updateEngineMetrics(&engine, duration, err == nil) - if err != nil { - log.Printf("Error performing search with %s: %v", engine.Name, err) - return nil - } - results = append(results, searchResults...) - } - - return results -} - -func fetchSearchFromCrawler(engine SearchEngine, query, safe, lang, searchType string, page int) ([]SearchResult, time.Duration, error) { - url := fmt.Sprintf("http://%s:%d/search?q=%s&safe=%s&lang=%s&t=%s&p=%d", engine.Host, engine.Port, query, safe, lang, searchType, page) - start := time.Now() - resp, err := http.Get(url) - if err != nil { - return nil, 0, err - } - defer resp.Body.Close() - - var results []SearchResult - if err := json.NewDecoder(resp.Body).Decode(&results); err != nil { - return nil, 0, err - } - - return results, time.Since(start), nil -} diff --git a/static/images/placeholder.svg b/static/images/placeholder.svg index fb7b0f8..41256d5 100644 --- a/static/images/placeholder.svg +++ b/static/images/placeholder.svg @@ -1,17 +1,2 @@ - - - - - image-picture - Created with Sketch Beta. - - - - - - - - - - - \ No newline at end of file + + \ No newline at end of file diff --git a/templates/images.html b/templates/images.html index 91fd5de..c290575 100755 --- a/templates/images.html +++ b/templates/images.html @@ -78,9 +78,10 @@ src="/static/images/placeholder.svg" data-id="{{ $result.ID }}" alt="{{ .Title }}" - data-media="{{ .Media }}" + data-full="{{ .ProxyFull }}" + data-proxy-full="{{ .ProxyThumb }}" class="clickable" - > + />
{{ .Width }} × {{ .Height }}
{{ .Title }} @@ -159,12 +160,12 @@ if (!parentImageDiv) return; const imgElement = parentImageDiv.querySelector('img.clickable'); - const mediaUrl = imgElement.dataset.media; // Full-size image URL - const proxyMediaUrl = imgElement.dataset.proxyMedia || imgElement.src; // Proxied full-size image URL or thumbnail proxy + 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 website URL + const sourceUrl = parentImageDiv.querySelector('.img_source').href; // Source webpage URL - if (!mediaUrl || viewerOpen) { + if (!fullImageUrl || viewerOpen) { return; // Don't open if data is missing or viewer is already open } viewerOpen = true; @@ -175,11 +176,12 @@ const fullSizeLink = imageView.querySelector('.full-size'); const proxySizeLink = imageView.querySelector('.proxy-size'); - viewerImage.src = mediaUrl; + // Set the viewer image to ProxyFull + viewerImage.src = fullImageUrl; viewerTitle.textContent = title; viewerSourceButton.href = sourceUrl; - fullSizeLink.href = sourceUrl; // Link to the source website - proxySizeLink.href = proxyMediaUrl; // Link to the proxied full-size image + 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'); @@ -221,6 +223,7 @@ } }); }); +