package main import ( "fmt" "net/http" "time" ) var textSearchEngines []SearchEngine func init() { textSearchEngines = []SearchEngine{ {Name: "Google", Func: wrapTextSearchFunc(PerformGoogleTextSearch)}, {Name: "LibreX", Func: wrapTextSearchFunc(PerformLibreXTextSearch)}, {Name: "Brave", Func: wrapTextSearchFunc(PerformBraveTextSearch)}, {Name: "DuckDuckGo", Func: wrapTextSearchFunc(PerformDuckDuckGoTextSearch)}, // {Name: "SearXNG", Func: wrapTextSearchFunc(PerformSearXNGTextSearch), Weight: 2}, // Uncomment when implemented } } func HandleTextSearch(w http.ResponseWriter, settings UserSettings, query string, page int) { startTime := time.Now() cacheKey := CacheKey{Query: query, Page: page, Safe: settings.SafeSearch == "active", Lang: settings.SearchLanguage, Type: "text"} combinedResults := getTextResultsFromCacheOrFetch(cacheKey, query, settings.SafeSearch, settings.SearchLanguage, page) hasPrevPage := page > 1 // Prefetch next and previous pages asynchronously go prefetchPage(query, settings.SafeSearch, settings.SearchLanguage, page+1) if hasPrevPage { go prefetchPage(query, settings.SafeSearch, settings.SearchLanguage, page-1) } elapsedTime := time.Since(startTime) // Prepare the data to pass to the template data := map[string]interface{}{ "Results": combinedResults, "Query": query, "Fetched": fmt.Sprintf("%.2f %s", elapsedTime.Seconds(), Translate("seconds")), // Time for fetching results "Page": page, "HasPrevPage": page > 1, "HasNextPage": len(combinedResults) >= 50, "NoResults": len(combinedResults) == 0, "LanguageOptions": languageOptions, "CurrentLang": settings.SearchLanguage, "Theme": settings.Theme, "Safe": settings.SafeSearch, "IsThemeDark": settings.IsThemeDark, "Trans": Translate, } // Render the template without measuring time renderTemplate(w, "text.html", data) } func getTextResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string, page int) []TextSearchResult { cacheChan := make(chan []SearchResult) var combinedResults []TextSearchResult go func() { results, exists := resultsCache.Get(cacheKey) if exists { printDebug("Cache hit") cacheChan <- results } else { printDebug("Cache miss") cacheChan <- nil } }() select { case results := <-cacheChan: if results == nil { // Fetch only if the cache miss occurs and Crawler is enabled if config.CrawlerEnabled { combinedResults = fetchTextResults(query, safe, lang, page) if len(combinedResults) > 0 { resultsCache.Set(cacheKey, convertToSearchResults(combinedResults)) } } else { printInfo("Crawler disabled; skipping fetching.") } } else { textResults, _, _ := convertToSpecificResults(results) combinedResults = textResults } case <-time.After(2 * time.Second): printInfo("Cache check timeout") if config.CrawlerEnabled { combinedResults = fetchTextResults(query, safe, lang, page) if len(combinedResults) > 0 { resultsCache.Set(cacheKey, convertToSearchResults(combinedResults)) } } else { printInfo("Crawler disabled; skipping fetching.") } } return combinedResults } func prefetchPage(query, safe, lang string, page int) { cacheKey := CacheKey{Query: query, Page: page, Safe: safe == "active", Lang: lang, Type: "text"} if _, exists := resultsCache.Get(cacheKey); !exists { printInfo("Page %d not cached, caching now...", page) if config.CrawlerEnabled { pageResults := fetchTextResults(query, safe, lang, page) if len(pageResults) > 0 { resultsCache.Set(cacheKey, convertToSearchResults(pageResults)) } } else { printInfo("Crawler disabled; skipping prefetch for page %d", page) } } else { printInfo("Page %d already cached", page) } } func fetchTextResults(query, safe, lang string, page int) []TextSearchResult { var results []TextSearchResult // If Crawler is disabled, do not fetch from search engines if !config.CrawlerEnabled { printDebug("Crawler is disabled; skipping search engine fetching.") return results // Return an empty list } engineCount := len(textSearchEngines) // Determine which engine to use for the current page engineIndex := (page - 1) % engineCount engine := textSearchEngines[engineIndex] // Calculate the page number for this engine enginePage := (page-1)/engineCount + 1 // Debug print to verify engine and page number being fetched printDebug("Fetching results for overall page %d using engine: %s (engine page %d)", page, engine.Name, enginePage) // Fetch results from the selected engine searchResults, _, err := engine.Func(query, safe, lang, enginePage) if err != nil { printWarn("Error performing search with %s: %v", engine.Name, err) } else { results = append(results, validateResults(searchResults)...) } // If no results are found with the selected engine, try the next in line if len(results) == 0 { for i := 1; i < engineCount; i++ { nextEngine := textSearchEngines[(engineIndex+i)%engineCount] enginePage = (page-1)/engineCount + 1 // Recalculate for the new engine printInfo("No results found, trying next engine: %s (engine page %d)", nextEngine.Name, enginePage) searchResults, _, err := nextEngine.Func(query, safe, lang, enginePage) if err != nil { printWarn("Error performing search with %s: %v", nextEngine.Name, err) continue } results = append(results, validateResults(searchResults)...) if len(results) > 0 { break } } } printInfo("Fetched %d results for overall page %d", len(results), page) return results } func validateResults(searchResults []SearchResult) []TextSearchResult { var validResults []TextSearchResult // Remove anything that is missing a URL or Header for _, result := range searchResults { textResult := result.(TextSearchResult) if textResult.URL != "" || textResult.Header != "" { validResults = append(validResults, textResult) } } return validResults } func wrapTextSearchFunc(f func(string, string, string, int) ([]TextSearchResult, time.Duration, error)) func(string, string, string, int) ([]SearchResult, time.Duration, error) { return func(query, safe, lang string, page int) ([]SearchResult, time.Duration, error) { textResults, duration, err := f(query, safe, lang, page) if err != nil { return nil, duration, err } searchResults := make([]SearchResult, len(textResults)) for i, result := range textResults { searchResults[i] = result } return searchResults, duration, nil } }