Search/text.go
partisan 5032173609
Some checks failed
Run Integration Tests / test (push) Failing after 41s
Added default globe.svg for invalid favicons
2025-05-30 23:14:49 +02:00

269 lines
8.5 KiB
Go
Executable file

package main
import (
"fmt"
"net/http"
"os"
"path/filepath"
"time"
)
var textSearchEngines []SearchEngine
var allTextSearchEngines = []SearchEngine{
{Name: "Google", Func: wrapTextSearchFunc(PerformGoogleTextSearch)},
{Name: "LibreX", Func: wrapTextSearchFunc(PerformLibreXTextSearch)},
{Name: "Brave", Func: wrapTextSearchFunc(PerformBraveTextSearch)},
{Name: "DuckDuckGo", Func: wrapTextSearchFunc(PerformDuckDuckGoTextSearch)},
{Name: "Quant", Func: wrapTextSearchFunc(PerformQwantTextSearch)}, // Broken !
//{Name: "SearXNG", Func: wrapTextSearchFunc(PerformSearXTextSearch)}, // bruh
}
func initTextEngines() {
// textSearchEngines is your final slice (already declared globally)
textSearchEngines = nil // or make([]SearchEngine, 0)
for _, engineName := range config.MetaSearch.Text {
for _, candidate := range allTextSearchEngines {
if candidate.Name == engineName {
textSearchEngines = append(textSearchEngines, candidate)
break
}
}
}
}
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)
// Simplified result structure without waiting for favicons
type DecoratedResult struct {
TextSearchResult
PrettyLink LinkParts
FaviconID string // Just the ID, URL will be generated client-side
}
var decoratedResults []DecoratedResult
for _, r := range combinedResults {
if r.URL == "" {
continue
}
prettyLink := FormatLinkHTML(r.URL)
faviconID := faviconIDFromURL(prettyLink.RootURL)
decoratedResults = append(decoratedResults, DecoratedResult{
TextSearchResult: r,
PrettyLink: prettyLink,
FaviconID: faviconID,
})
// Start async favicon fetch if not already cached
go ensureFaviconIsCached(faviconID, prettyLink.RootURL)
}
data := map[string]interface{}{
"Results": decoratedResults,
"Query": query,
"Fetched": FormatElapsedTime(elapsedTime),
"Page": page,
"HasPrevPage": hasPrevPage,
"HasNextPage": len(combinedResults) >= 50,
"NoResults": len(combinedResults) == 0,
"LanguageOptions": languageOptions,
"CurrentLang": settings.SearchLanguage,
"Theme": settings.Theme,
"Safe": settings.SafeSearch,
"IsThemeDark": settings.IsThemeDark,
"Trans": Translate,
"HardCacheEnabled": config.DriveCacheEnabled,
}
renderTemplate(w, "text.html", data)
}
func ensureFaviconIsCached(faviconID, rootURL string) {
// Check if already exists in cache
filename := fmt.Sprintf("%s_icon.webp", faviconID)
cachedPath := filepath.Join(config.DriveCache.Path, "images", filename)
if _, err := os.Stat(cachedPath); err == nil {
return // Already cached
}
// Not cached, initiate download
getFaviconProxyURL("", rootURL) // This will trigger async download
}
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 {
// Always attempt to fetch results on a cache miss
combinedResults = fetchTextResults(query, safe, lang, page)
if len(combinedResults) > 0 {
resultsCache.Set(cacheKey, convertToSearchResults(combinedResults))
}
} else {
textResults, _, _, _, _ := convertToSpecificResults(results)
combinedResults = textResults
}
case <-time.After(2 * time.Second):
printInfo("Cache check timeout")
// Even on timeout, attempt to fetch results
combinedResults = fetchTextResults(query, safe, lang, page)
if len(combinedResults) > 0 {
resultsCache.Set(cacheKey, convertToSearchResults(combinedResults))
}
}
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.MetaSearchEnabled {
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)
}
}
// The logic in this function is rotating search engines instead of running them in order as noted in the wiki
func fetchTextResults(query, safe, lang string, page int) []TextSearchResult {
var results []TextSearchResult
if !config.MetaSearchEnabled {
printDebug("Crawler is disabled; fetching from local index.")
// Calculate the starting position based on the page number
indexedResults, err := SearchIndex(query, page, 10)
if err != nil {
printErr("Error searching the index: %v", err)
return results // Return empty results on error
}
// Convert indexed results to TextSearchResult format
for _, doc := range indexedResults {
results = append(results, TextSearchResult{
URL: doc.Link,
Header: doc.Title,
Description: doc.Description,
Source: doc.Tags,
})
}
return results
} else {
// Crawler is enabled, so use the search engines
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
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
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
}
}