Compare commits
No commits in common. "main" and "v0.1.0" have entirely different histories.
30 changed files with 369 additions and 1544 deletions
|
@ -32,12 +32,12 @@ A self-hosted private and anonymous [metasearch engine](https://en.wikipedia.org
|
|||
## Comparison to other search engines
|
||||
|
||||
|
||||
| Feature | Whoogle [1] | Araa-Search | LibreY | 4get | *Warp* |
|
||||
| Feature | Whoogle | Araa-Search | LibreY | 4get | *Warp* |
|
||||
| :----------------------------------- | ------------------ | ------------------------- | ------------------------ | ------------------------ | ---------------------------------------------------- |
|
||||
| Works without JavaScript | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Music search | ❓ | ❌ | ❌ | ✅ | ✅ |
|
||||
| Torrent search | ❌ | ✅ | ✅ | ❌ | ✅ |
|
||||
| API | ❌ | ✅ | ❓ [2] | ✅ | ✅ |
|
||||
| API | ❌ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Scalable | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| Not Resource Hungry | ❓ Moderate | ❌ Very resource hungry | ❌ Moderate 200-400mb~ | ❌ Moderate 200-400mb~ | ✅ about 15-20MiB at idle, 17-22MiB when searching |
|
||||
| Dynamic Page Loading | ❓ Not specified | ❌ | ❌ | ❌ | ✅ |
|
||||
|
|
8
files.go
8
files.go
|
@ -41,8 +41,8 @@ func initializeTorrentSites() {
|
|||
func handleFileSearch(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: "file"}
|
||||
combinedResults := getFileResultsFromCacheOrFetch(cacheKey, query, settings.SafeSearch, settings.SearchLanguage, page)
|
||||
cacheKey := CacheKey{Query: query, Page: page, Safe: settings.SafeSearch == "true", Lang: settings.Language, Type: "file"}
|
||||
combinedResults := getFileResultsFromCacheOrFetch(cacheKey, query, settings.SafeSearch, settings.Language, page)
|
||||
|
||||
sort.Slice(combinedResults, func(i, j int) bool { return combinedResults[i].Seeders > combinedResults[j].Seeders })
|
||||
|
||||
|
@ -71,7 +71,6 @@ func handleFileSearch(w http.ResponseWriter, settings UserSettings, query string
|
|||
CurrentLang string
|
||||
Theme string
|
||||
Safe string
|
||||
IsThemeDark bool
|
||||
}{
|
||||
Results: combinedResults,
|
||||
Query: query,
|
||||
|
@ -82,10 +81,9 @@ func handleFileSearch(w http.ResponseWriter, settings UserSettings, query string
|
|||
HasPrevPage: page > 1,
|
||||
HasNextPage: len(combinedResults) > 0,
|
||||
LanguageOptions: languageOptions,
|
||||
CurrentLang: settings.SearchLanguage,
|
||||
CurrentLang: settings.Language,
|
||||
Theme: settings.Theme,
|
||||
Safe: settings.SafeSearch,
|
||||
IsThemeDark: settings.IsThemeDark,
|
||||
}
|
||||
|
||||
// // Debugging: Print results before rendering template
|
||||
|
|
|
@ -102,7 +102,7 @@ func handleForumsSearch(w http.ResponseWriter, settings UserSettings, query stri
|
|||
results, err := PerformRedditSearch(query, settings.SafeSearch, page)
|
||||
if err != nil || len(results) == 0 { // 0 == 0 to force search by other node
|
||||
log.Printf("No results from primary search, trying other nodes")
|
||||
results = tryOtherNodesForForumSearch(query, settings.SafeSearch, settings.SearchLanguage, page)
|
||||
results = tryOtherNodesForForumSearch(query, settings.SafeSearch, settings.Language, page)
|
||||
}
|
||||
|
||||
data := struct {
|
||||
|
@ -115,7 +115,6 @@ func handleForumsSearch(w http.ResponseWriter, settings UserSettings, query stri
|
|||
CurrentLang string
|
||||
Theme string
|
||||
Safe string
|
||||
IsThemeDark bool
|
||||
}{
|
||||
Query: query,
|
||||
Results: results,
|
||||
|
@ -123,10 +122,9 @@ func handleForumsSearch(w http.ResponseWriter, settings UserSettings, query stri
|
|||
HasPrevPage: page > 1,
|
||||
HasNextPage: len(results) == 25,
|
||||
LanguageOptions: languageOptions,
|
||||
CurrentLang: settings.SearchLanguage,
|
||||
CurrentLang: settings.Language,
|
||||
Theme: settings.Theme,
|
||||
Safe: settings.SafeSearch,
|
||||
IsThemeDark: settings.IsThemeDark,
|
||||
}
|
||||
|
||||
funcMap := template.FuncMap{
|
||||
|
|
3
go.mod
3
go.mod
|
@ -23,9 +23,6 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
github.com/chai2010/webp v1.1.1 // indirect
|
||||
github.com/disintegration/imaging v1.6.2 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
golang.org/x/image v0.20.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
)
|
||||
|
|
8
go.sum
8
go.sum
|
@ -2,16 +2,12 @@ github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VP
|
|||
github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk=
|
||||
github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
|
||||
github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732 h1:XYUCaZrW8ckGWlCRJKCSoh/iFwlpX316a8yY9IFEzv8=
|
||||
github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||
github.com/chromedp/chromedp v0.9.5 h1:viASzruPJOiThk7c5bueOUY91jGLJVximoEMGoH93rg=
|
||||
github.com/chromedp/chromedp v0.9.5/go.mod h1:D4I2qONslauw/C7INoCir1BJkSwBYMyZgx8X276z3+Y=
|
||||
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
||||
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
|
@ -35,10 +31,6 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
|
|||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw=
|
||||
golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
@ -13,12 +14,20 @@ func handleImageProxy(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// Fetch the image from the external URL
|
||||
resp, err := http.Get(imageURL)
|
||||
if err != nil {
|
||||
printWarn("Error fetching image: %v", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
// Try to fetch the image from Bing first
|
||||
bingURL := fmt.Sprintf("https://tse.mm.bing.net/th?q=%s", imageURL)
|
||||
resp, err := http.Get(bingURL)
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
// If fetching from Bing fails, attempt to fetch from the original image URL
|
||||
printWarn("Error fetching image from Bing, trying original URL.")
|
||||
|
||||
// Attempt to fetch the image directly
|
||||
resp, err = http.Get(imageURL)
|
||||
if err != nil {
|
||||
printWarn("Error fetching image: %v", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
@ -12,7 +11,6 @@ import (
|
|||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
// PerformBingImageSearch performs a Bing image search and returns the results.
|
||||
func PerformBingImageSearch(query, safe, lang string, page int) ([]ImageSearchResult, time.Duration, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
|
@ -38,61 +36,51 @@ 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
|
||||
doc.Find(".imgpt").Each(func(i int, s *goquery.Selection) {
|
||||
imgTag := s.Find("img")
|
||||
imgSrc, exists := imgTag.Attr("src")
|
||||
if !exists {
|
||||
imgSrc, exists = imgTag.Attr("data-src")
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Extract the image title from `alt` attribute
|
||||
title := imgTag.AttrOr("alt", "")
|
||||
title, _ := imgTag.Attr("alt")
|
||||
|
||||
// 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 (the original image source)
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(metadata), &data); err == nil {
|
||||
mediaURL, ok := data["murl"].(string)
|
||||
if ok {
|
||||
// Apply the image proxy
|
||||
proxiedURL := "/imgproxy?url=" + mediaURL
|
||||
results = append(results, ImageSearchResult{
|
||||
Thumbnail: imgSrc,
|
||||
Title: strings.TrimSpace(title),
|
||||
Media: mediaURL,
|
||||
Source: mediaURL,
|
||||
ThumbProxy: proxiedURL, // Use the proxied URL
|
||||
Width: width,
|
||||
Height: height,
|
||||
})
|
||||
// Extract the original image URL from the `mediaurl` parameter in the link
|
||||
pageLink, exists := s.Find("a.iusc").Attr("href")
|
||||
mediaURL := ""
|
||||
if exists {
|
||||
if u, err := url.Parse(pageLink); err == nil {
|
||||
if mediaURLParam := u.Query().Get("mediaurl"); mediaURLParam != "" {
|
||||
mediaURL, _ = url.QueryUnescape(mediaURLParam)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, ImageSearchResult{
|
||||
Thumbnail: imgSrc,
|
||||
Title: strings.TrimSpace(title),
|
||||
Media: imgSrc,
|
||||
Width: width,
|
||||
Height: height,
|
||||
Source: mediaURL, // Original image URL
|
||||
ThumbProxy: imgSrc,
|
||||
})
|
||||
})
|
||||
|
||||
duration := time.Since(startTime)
|
||||
|
||||
// Check if the number of results is one or less
|
||||
if len(results) == 0 {
|
||||
if len(results) <= 1 {
|
||||
return nil, duration, fmt.Errorf("no images found")
|
||||
}
|
||||
|
||||
return results, duration, nil
|
||||
}
|
||||
|
||||
// buildBingSearchURL constructs the search URL for Bing Image Search
|
||||
func buildBingSearchURL(query string, page int) string {
|
||||
baseURL := "https://www.bing.com/images/search"
|
||||
params := url.Values{}
|
||||
|
@ -103,7 +91,6 @@ func buildBingSearchURL(query string, page int) string {
|
|||
return baseURL + "?" + params.Encode()
|
||||
}
|
||||
|
||||
// Example usage in main (commented out for clarity)
|
||||
// func main() {
|
||||
// results, duration, err := PerformBingImageSearch("kittens", "false", "en", 1)
|
||||
// if err != nil {
|
||||
|
|
|
@ -1,237 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
// NextPageCache is a specialized cache for storing next page links
|
||||
type NextPageCache struct {
|
||||
mu sync.Mutex
|
||||
links map[string]string
|
||||
expiration time.Duration
|
||||
}
|
||||
|
||||
// NewNextPageCache creates a new NextPageCache with a specified expiration duration
|
||||
func NewNextPageCache(expiration time.Duration) *NextPageCache {
|
||||
return &NextPageCache{
|
||||
links: make(map[string]string),
|
||||
expiration: expiration,
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves the next page link for a given key from the cache
|
||||
func (npc *NextPageCache) Get(key CacheKey) (string, bool) {
|
||||
npc.mu.Lock()
|
||||
defer npc.mu.Unlock()
|
||||
|
||||
link, exists := npc.links[npc.keyToString(key)]
|
||||
if !exists {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return link, true
|
||||
}
|
||||
|
||||
// Set stores the next page link for a given key in the cache
|
||||
// Idk it maybye worth it to use "cache.go" for this
|
||||
func (npc *NextPageCache) Set(key CacheKey, link string) {
|
||||
npc.mu.Lock()
|
||||
defer npc.mu.Unlock()
|
||||
|
||||
npc.links[npc.keyToString(key)] = link
|
||||
}
|
||||
|
||||
// keyToString converts a CacheKey to a string representation
|
||||
func (npc *NextPageCache) keyToString(key CacheKey) string {
|
||||
return fmt.Sprintf("%s|%d|%t|%s|%s", key.Query, key.Page, key.Safe, key.Lang, key.Type)
|
||||
}
|
||||
|
||||
var (
|
||||
nextPageCache = NewNextPageCache(6 * time.Hour) // Cache with 6-hour expiration
|
||||
)
|
||||
|
||||
// PerformDeviantArtImageSearch performs a search on DeviantArt and returns a list of image results
|
||||
func PerformDeviantArtImageSearch(query, safe, lang string, page int) ([]ImageSearchResult, time.Duration, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
cacheKey := CacheKey{
|
||||
Query: query,
|
||||
Page: page,
|
||||
Safe: safe == "active",
|
||||
Lang: lang,
|
||||
Type: "deviantart",
|
||||
}
|
||||
|
||||
// Check if the next page link is cached
|
||||
var searchURL string
|
||||
if page > 1 {
|
||||
if nextPageLink, found := nextPageCache.Get(cacheKey); found {
|
||||
searchURL = nextPageLink
|
||||
} else {
|
||||
return nil, 0, fmt.Errorf("next page link not found in cache")
|
||||
}
|
||||
} else {
|
||||
searchURL = buildDeviantArtSearchURL(query, page)
|
||||
}
|
||||
|
||||
// Get the User-Agent string
|
||||
DeviantArtImageUserAgent, err := GetUserAgent("Image-Search-DeviantArt")
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Make the HTTP request with User-Agent header
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", searchURL, nil)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("creating request: %v", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", DeviantArtImageUserAgent)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("making request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, 0, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Parse the HTML document
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("loading HTML document: %v", err)
|
||||
}
|
||||
|
||||
// Channel to receive valid image results
|
||||
resultsChan := make(chan ImageSearchResult)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Extract data using goquery
|
||||
doc.Find("div._2pZkk div div a").Each(func(i int, s *goquery.Selection) {
|
||||
// Skip images that are blurred (premium content)
|
||||
premiumText := s.Find("../div/div/div").Text()
|
||||
if strings.Contains(premiumText, "Watch the artist to view this deviation") {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract image source, fallback on data-src if necessary
|
||||
imgSrc, exists := s.Find("div img").Attr("srcset")
|
||||
if !exists {
|
||||
imgSrc, exists = s.Find("div img").Attr("data-src")
|
||||
}
|
||||
if !exists || imgSrc == "" {
|
||||
return
|
||||
}
|
||||
imgSrc = strings.Split(imgSrc, " ")[0]
|
||||
parsedURL, err := url.Parse(imgSrc)
|
||||
if err == nil {
|
||||
parts := strings.Split(parsedURL.Path, "/v1")
|
||||
parsedURL.Path = parts[0]
|
||||
imgSrc = parsedURL.String()
|
||||
}
|
||||
|
||||
// Extract URL and title
|
||||
resultURL := s.AttrOr("href", "")
|
||||
title := s.AttrOr("aria-label", "")
|
||||
|
||||
// Only proceed if title, URL, and img_src are not empty
|
||||
if title != "" && resultURL != "" && imgSrc != "" {
|
||||
wg.Add(1)
|
||||
go func(imgSrc, resultURL, title string) {
|
||||
defer wg.Done()
|
||||
// Verify if the image URL is accessible
|
||||
if isValidImageURL(imgSrc, DeviantArtImageUserAgent, resultURL) {
|
||||
resultsChan <- ImageSearchResult{
|
||||
Title: strings.TrimSpace(title),
|
||||
Media: imgSrc,
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
Source: resultURL,
|
||||
ThumbProxy: "/imgproxy?url=" + imgSrc,
|
||||
}
|
||||
}
|
||||
}(imgSrc, resultURL, title)
|
||||
}
|
||||
})
|
||||
|
||||
// Close the results channel when all goroutines are done
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultsChan)
|
||||
}()
|
||||
|
||||
// Collect results from the channel
|
||||
var results []ImageSearchResult
|
||||
for result := range resultsChan {
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
// Cache the next page link, if any
|
||||
nextPageLink := doc.Find("a._1OGeq").Last().AttrOr("href", "")
|
||||
if nextPageLink != "" {
|
||||
nextPageCache.Set(cacheKey, nextPageLink)
|
||||
}
|
||||
|
||||
duration := time.Since(startTime)
|
||||
|
||||
// Check if the number of results is one or less
|
||||
if len(results) == 0 {
|
||||
return nil, duration, fmt.Errorf("no images found")
|
||||
}
|
||||
|
||||
return results, duration, nil
|
||||
}
|
||||
|
||||
// buildDeviantArtSearchURL builds the search URL for DeviantArt
|
||||
func buildDeviantArtSearchURL(query string, page int) string {
|
||||
baseURL := "https://www.deviantart.com/search"
|
||||
params := url.Values{}
|
||||
params.Add("q", query)
|
||||
return baseURL + "?" + params.Encode()
|
||||
}
|
||||
|
||||
// isValidImageURL checks if the image URL is accessible with the provided User-Agent
|
||||
func isValidImageURL(imgSrc, userAgent, referer string) bool {
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("HEAD", imgSrc, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Set headers to mimic a regular browser request
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
req.Header.Set("Referer", referer)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return resp.StatusCode == http.StatusOK
|
||||
}
|
||||
|
||||
// // Example usage:
|
||||
// func main() {
|
||||
// results, duration, err := PerformDeviantArtImageSearch("kittens", "false", "en", 1)
|
||||
// if err != nil {
|
||||
// fmt.Println("Error:", err)
|
||||
// return
|
||||
// }
|
||||
|
||||
// fmt.Printf("Search took: %v\n", duration)
|
||||
// fmt.Printf("Total results: %d\n", len(results))
|
||||
// for _, result := range results {
|
||||
// fmt.Printf("Title: %s\nThumbnail: %s\nMedia: %s\nSource (Original Image URL): %s\n\n",
|
||||
// result.Title, result.Thumbnail, result.Media, result.Source)
|
||||
// }
|
||||
// }
|
110
images-quant.go
110
images-quant.go
|
@ -5,7 +5,6 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -25,64 +24,6 @@ type QwantAPIResponse struct {
|
|||
} `json:"data"`
|
||||
}
|
||||
|
||||
func ConvertToQwantLocale(langCode string) string {
|
||||
langMap := map[string]string{
|
||||
"en": "en_us", // English
|
||||
"af": "en_us", // Afrikaans (no direct match, default to English)
|
||||
"ar": "ar", // Arabic
|
||||
"hy": "en_us", // Armenian (no direct match, default to English)
|
||||
"be": "en_us", // Belarusian (no direct match, default to English)
|
||||
"bg": "bg_bg", // Bulgarian
|
||||
"ca": "ca_es", // Catalan
|
||||
"zh-CN": "zh_cn", // Chinese Simplified
|
||||
"zh-TW": "zh_hk", // Chinese Traditional
|
||||
"hr": "en_us", // Croatian (no direct match, default to English)
|
||||
"cs": "cs_cz", // Czech
|
||||
"da": "da_dk", // Danish
|
||||
"nl": "nl_nl", // Dutch
|
||||
"eo": "en_us", // Esperanto (no direct match, default to English)
|
||||
"et": "et_ee", // Estonian
|
||||
"tl": "en_us", // Tagalog (no direct match, default to English)
|
||||
"fi": "fi_fi", // Finnish
|
||||
"fr": "fr_fr", // French
|
||||
"de": "de_de", // German
|
||||
"el": "el_gr", // Greek
|
||||
"iw": "he_il", // Hebrew
|
||||
"hi": "en_us", // Hindi (no direct match, default to English)
|
||||
"hu": "hu_hu", // Hungarian
|
||||
"is": "en_us", // Icelandic (no direct match, default to English)
|
||||
"id": "en_us", // Indonesian (no direct match, default to English)
|
||||
"it": "it_it", // Italian
|
||||
"ja": "ja_jp", // Japanese
|
||||
"ko": "ko_kr", // Korean
|
||||
"lv": "en_us", // Latvian (no direct match, default to English)
|
||||
"lt": "en_us", // Lithuanian (no direct match, default to English)
|
||||
"no": "nb_no", // Norwegian
|
||||
"fa": "en_us", // Persian (no direct match, default to English)
|
||||
"pl": "pl_pl", // Polish
|
||||
"pt": "pt_pt", // Portuguese
|
||||
"ro": "ro_ro", // Romanian
|
||||
"ru": "ru_ru", // Russian
|
||||
"sr": "en_us", // Serbian (no direct match, default to English)
|
||||
"sk": "en_us", // Slovak (no direct match, default to English)
|
||||
"sl": "en_us", // Slovenian (no direct match, default to English)
|
||||
"es": "es_es", // Spanish
|
||||
"sw": "en_us", // Swahili (no direct match, default to English)
|
||||
"sv": "sv_se", // Swedish
|
||||
"th": "th_th", // Thai
|
||||
"tr": "tr_tr", // Turkish
|
||||
"uk": "en_us", // Ukrainian (no direct match, default to English)
|
||||
"vi": "en_us", // Vietnamese (no direct match, default to English)
|
||||
"": "en_us", // Default to English if no language is provided
|
||||
}
|
||||
|
||||
if qwantLocale, exists := langMap[langCode]; exists {
|
||||
return qwantLocale
|
||||
}
|
||||
printWarn("Qwant locale code missing: %v, defaulting to: en_us", langCode)
|
||||
return "en_us" // Default fallback
|
||||
}
|
||||
|
||||
// PerformQwantImageSearch performs an image search on Qwant and returns the results.
|
||||
func PerformQwantImageSearch(query, safe, lang string, page int) ([]ImageSearchResult, time.Duration, error) {
|
||||
startTime := time.Now() // Start the timer
|
||||
|
@ -95,16 +36,13 @@ func PerformQwantImageSearch(query, safe, lang string, page int) ([]ImageSearchR
|
|||
offset = (page - 1) * resultsPerPage
|
||||
}
|
||||
|
||||
// Ensure count + offset is within acceptable limits
|
||||
if offset+resultsPerPage > 250 {
|
||||
return nil, 0, fmt.Errorf("count + offset must be lower than 250 for quant")
|
||||
}
|
||||
|
||||
if safe == "" {
|
||||
safe = "0"
|
||||
}
|
||||
|
||||
lang = ConvertToQwantLocale(lang)
|
||||
if lang == "" {
|
||||
lang = "en_CA"
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("https://api.qwant.com/v3/search/images?t=images&q=%s&count=%d&locale=%s&offset=%d&device=desktop&tgp=2&safesearch=%s",
|
||||
url.QueryEscape(query),
|
||||
|
@ -120,7 +58,7 @@ func PerformQwantImageSearch(query, safe, lang string, page int) ([]ImageSearchR
|
|||
return nil, 0, fmt.Errorf("creating request: %v", err)
|
||||
}
|
||||
|
||||
ImageUserAgent, err := GetUserAgent("Image-Search-Quant")
|
||||
ImageUserAgent, err := GetUserAgent("Image-Search")
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
@ -142,37 +80,19 @@ func PerformQwantImageSearch(query, safe, lang string, page int) ([]ImageSearchR
|
|||
return nil, 0, fmt.Errorf("decoding response: %v", err)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
results := make([]ImageSearchResult, len(apiResp.Data.Result.Items))
|
||||
|
||||
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"`
|
||||
}) {
|
||||
defer wg.Done()
|
||||
|
||||
// Populate the result
|
||||
results[i] = ImageSearchResult{
|
||||
Thumbnail: item.Thumbnail,
|
||||
Title: item.Title,
|
||||
Media: item.Media,
|
||||
Source: item.Url,
|
||||
ThumbProxy: "/imgproxy?url=" + item.Media,
|
||||
Width: item.Width,
|
||||
Height: item.Height,
|
||||
}
|
||||
}(i, item)
|
||||
var results []ImageSearchResult
|
||||
for _, item := range apiResp.Data.Result.Items {
|
||||
results = append(results, ImageSearchResult{
|
||||
Thumbnail: item.Thumbnail,
|
||||
Title: item.Title,
|
||||
Media: item.Media,
|
||||
Source: item.Url,
|
||||
ThumbProxy: "/img_proxy?url=" + url.QueryEscape(item.Media),
|
||||
Width: item.Width,
|
||||
Height: item.Height,
|
||||
})
|
||||
}
|
||||
|
||||
// Wait for all goroutines to complete
|
||||
wg.Wait()
|
||||
|
||||
duration := time.Since(startTime) // Calculate the duration
|
||||
|
||||
return results, duration, nil
|
||||
|
|
13
images.go
13
images.go
|
@ -13,17 +13,16 @@ 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: "Imgur", Func: wrapImageSearchFunc(PerformImgurImageSearch), Weight: 4}, // Image proxy not working
|
||||
{Name: "Bing", Func: wrapImageSearchFunc(PerformBingImageSearch), Weight: 2}, // Bing sometimes returns with low amount of images, this leads to danamica page loading not working
|
||||
{Name: "Imgur", Func: wrapImageSearchFunc(PerformImgurImageSearch), Weight: 3},
|
||||
}
|
||||
}
|
||||
|
||||
func handleImageSearch(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: "image"}
|
||||
combinedResults := getImageResultsFromCacheOrFetch(cacheKey, query, settings.SafeSearch, settings.SearchLanguage, page)
|
||||
cacheKey := CacheKey{Query: query, Page: page, Safe: settings.SafeSearch == "true", Lang: settings.Language, Type: "image"}
|
||||
combinedResults := getImageResultsFromCacheOrFetch(cacheKey, query, settings.SafeSearch, settings.Language, page)
|
||||
|
||||
elapsedTime := time.Since(startTime)
|
||||
tmpl, err := template.New("images.html").Funcs(funcs).ParseFiles("templates/images.html")
|
||||
|
@ -45,7 +44,6 @@ func handleImageSearch(w http.ResponseWriter, settings UserSettings, query strin
|
|||
CurrentLang string
|
||||
Theme string
|
||||
Safe string
|
||||
IsThemeDark bool
|
||||
}{
|
||||
Results: combinedResults,
|
||||
Query: query,
|
||||
|
@ -55,10 +53,9 @@ func handleImageSearch(w http.ResponseWriter, settings UserSettings, query strin
|
|||
HasNextPage: len(combinedResults) >= 50,
|
||||
NoResults: len(combinedResults) == 0,
|
||||
LanguageOptions: languageOptions,
|
||||
CurrentLang: settings.SearchLanguage,
|
||||
CurrentLang: settings.Language,
|
||||
Theme: settings.Theme,
|
||||
Safe: settings.SafeSearch,
|
||||
IsThemeDark: settings.IsThemeDark,
|
||||
}
|
||||
|
||||
err = tmpl.Execute(w, data)
|
||||
|
|
153
main.go
153
main.go
|
@ -18,117 +18,101 @@ var settings UserSettings
|
|||
|
||||
var languageOptions = []LanguageOption{
|
||||
{Code: "", Name: "Any Language"},
|
||||
{Code: "en", Name: "English"},
|
||||
{Code: "af", Name: "Afrikaans"},
|
||||
{Code: "ar", Name: "العربية (Arabic)"},
|
||||
{Code: "hy", Name: "Հայերեն (Armenian)"},
|
||||
{Code: "be", Name: "Беларуская (Belarusian)"},
|
||||
{Code: "bg", Name: "български (Bulgarian)"},
|
||||
{Code: "ca", Name: "Català (Catalan)"},
|
||||
{Code: "zh-CN", Name: "中文 (简体) (Chinese Simplified)"},
|
||||
{Code: "zh-TW", Name: "中文 (繁體) (Chinese Traditional)"},
|
||||
{Code: "hr", Name: "Hrvatski (Croatian)"},
|
||||
{Code: "cs", Name: "Čeština (Czech)"},
|
||||
{Code: "da", Name: "Dansk (Danish)"},
|
||||
{Code: "nl", Name: "Nederlands (Dutch)"},
|
||||
{Code: "eo", Name: "Esperanto"},
|
||||
{Code: "et", Name: "Eesti (Estonian)"},
|
||||
{Code: "tl", Name: "Filipino (Tagalog)"},
|
||||
{Code: "fi", Name: "Suomi (Finnish)"},
|
||||
{Code: "fr", Name: "Français (French)"},
|
||||
{Code: "de", Name: "Deutsch (German)"},
|
||||
{Code: "el", Name: "Ελληνικά (Greek)"},
|
||||
{Code: "iw", Name: "עברית (Hebrew)"},
|
||||
{Code: "hi", Name: "हिन्दी (Hindi)"},
|
||||
{Code: "hu", Name: "magyar (Hungarian)"},
|
||||
{Code: "is", Name: "íslenska (Icelandic)"},
|
||||
{Code: "id", Name: "Bahasa Indonesia (Indonesian)"},
|
||||
{Code: "it", Name: "italiano (Italian)"},
|
||||
{Code: "ja", Name: "日本語 (Japanese)"},
|
||||
{Code: "ko", Name: "한국어 (Korean)"},
|
||||
{Code: "lv", Name: "latviešu (Latvian)"},
|
||||
{Code: "lt", Name: "lietuvių (Lithuanian)"},
|
||||
{Code: "no", Name: "norsk (Norwegian)"},
|
||||
{Code: "fa", Name: "فارسی (Persian)"},
|
||||
{Code: "pl", Name: "polski (Polish)"},
|
||||
{Code: "pt", Name: "português (Portuguese)"},
|
||||
{Code: "ro", Name: "română (Romanian)"},
|
||||
{Code: "ru", Name: "русский (Russian)"},
|
||||
{Code: "sr", Name: "српски (Serbian)"},
|
||||
{Code: "sk", Name: "slovenčina (Slovak)"},
|
||||
{Code: "sl", Name: "slovenščina (Slovenian)"},
|
||||
{Code: "es", Name: "español (Spanish)"},
|
||||
{Code: "sw", Name: "Kiswahili (Swahili)"},
|
||||
{Code: "sv", Name: "svenska (Swedish)"},
|
||||
{Code: "th", Name: "ไทย (Thai)"},
|
||||
{Code: "tr", Name: "Türkçe (Turkish)"},
|
||||
{Code: "uk", Name: "українська (Ukrainian)"},
|
||||
{Code: "vi", Name: "Tiếng Việt (Vietnamese)"},
|
||||
{Code: "lang_en", Name: "English"},
|
||||
{Code: "lang_af", Name: "Afrikaans"},
|
||||
{Code: "lang_ar", Name: "العربية (Arabic)"},
|
||||
{Code: "lang_hy", Name: "Հայերեն (Armenian)"},
|
||||
{Code: "lang_be", Name: "Беларуская (Belarusian)"},
|
||||
{Code: "lang_bg", Name: "български (Bulgarian)"},
|
||||
{Code: "lang_ca", Name: "Català (Catalan)"},
|
||||
{Code: "lang_zh-CN", Name: "中文 (简体) (Chinese Simplified)"},
|
||||
{Code: "lang_zh-TW", Name: "中文 (繁體) (Chinese Traditional)"},
|
||||
{Code: "lang_hr", Name: "Hrvatski (Croatian)"},
|
||||
{Code: "lang_cs", Name: "Čeština (Czech)"},
|
||||
{Code: "lang_da", Name: "Dansk (Danish)"},
|
||||
{Code: "lang_nl", Name: "Nederlands (Dutch)"},
|
||||
{Code: "lang_eo", Name: "Esperanto"},
|
||||
{Code: "lang_et", Name: "Eesti (Estonian)"},
|
||||
{Code: "lang_tl", Name: "Filipino (Tagalog)"},
|
||||
{Code: "lang_fi", Name: "Suomi (Finnish)"},
|
||||
{Code: "lang_fr", Name: "Français (French)"},
|
||||
{Code: "lang_de", Name: "Deutsch (German)"},
|
||||
{Code: "lang_el", Name: "Ελληνικά (Greek)"},
|
||||
{Code: "lang_iw", Name: "עברית (Hebrew)"},
|
||||
{Code: "lang_hi", Name: "हिन्दी (Hindi)"},
|
||||
{Code: "lang_hu", Name: "magyar (Hungarian)"},
|
||||
{Code: "lang_is", Name: "íslenska (Icelandic)"},
|
||||
{Code: "lang_id", Name: "Bahasa Indonesia (Indonesian)"},
|
||||
{Code: "lang_it", Name: "italiano (Italian)"},
|
||||
{Code: "lang_ja", Name: "日本語 (Japanese)"},
|
||||
{Code: "lang_ko", Name: "한국어 (Korean)"},
|
||||
{Code: "lang_lv", Name: "latviešu (Latvian)"},
|
||||
{Code: "lang_lt", Name: "lietuvių (Lithuanian)"},
|
||||
{Code: "lang_no", Name: "norsk (Norwegian)"},
|
||||
{Code: "lang_fa", Name: "فارسی (Persian)"},
|
||||
{Code: "lang_pl", Name: "polski (Polish)"},
|
||||
{Code: "lang_pt", Name: "português (Portuguese)"},
|
||||
{Code: "lang_ro", Name: "română (Romanian)"},
|
||||
{Code: "lang_ru", Name: "русский (Russian)"},
|
||||
{Code: "lang_sr", Name: "српски (Serbian)"},
|
||||
{Code: "lang_sk", Name: "slovenčina (Slovak)"},
|
||||
{Code: "lang_sl", Name: "slovenščina (Slovenian)"},
|
||||
{Code: "lang_es", Name: "español (Spanish)"},
|
||||
{Code: "lang_sw", Name: "Kiswahili (Swahili)"},
|
||||
{Code: "lang_sv", Name: "svenska (Swedish)"},
|
||||
{Code: "lang_th", Name: "ไทย (Thai)"},
|
||||
{Code: "lang_tr", Name: "Türkçe (Turkish)"},
|
||||
{Code: "lang_uk", Name: "українська (Ukrainian)"},
|
||||
{Code: "lang_vi", Name: "Tiếng Việt (Vietnamese)"},
|
||||
}
|
||||
|
||||
func handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||
query, safe, lang, searchType, page := parseSearchParams(r)
|
||||
|
||||
settings = loadUserSettings(w, r)
|
||||
// Load user settings
|
||||
settings = loadUserSettings(r)
|
||||
|
||||
// Update the theme, safe search, and language based on query parameters or use existing settings
|
||||
theme := r.URL.Query().Get("theme")
|
||||
if theme != "" {
|
||||
settings.Theme = theme
|
||||
saveUserSettings(w, settings)
|
||||
} else if settings.Theme == "" {
|
||||
settings.Theme = "dark"
|
||||
settings.Theme = "dark" // Default theme
|
||||
}
|
||||
|
||||
if safe != "" && safe != settings.SafeSearch {
|
||||
if safe != "" {
|
||||
settings.SafeSearch = safe
|
||||
saveUserSettings(w, settings)
|
||||
}
|
||||
|
||||
// Update site language if provided, or use existing settings
|
||||
if lang != "" && lang != settings.SiteLanguage {
|
||||
settings.SiteLanguage = lang
|
||||
saveUserSettings(w, settings)
|
||||
} else if settings.SiteLanguage == "" {
|
||||
settings.SiteLanguage = normalizeLangCode(r.Header.Get("Accept-Language"))
|
||||
if lang != "" {
|
||||
settings.Language = lang
|
||||
saveUserSettings(w, settings)
|
||||
}
|
||||
|
||||
// Update search language (can be empty)
|
||||
searchLang := r.URL.Query().Get("search_lang")
|
||||
if searchLang != settings.SearchLanguage {
|
||||
settings.SearchLanguage = searchLang
|
||||
saveUserSettings(w, settings)
|
||||
}
|
||||
// Render the search page template if no query
|
||||
|
||||
switch settings.Theme {
|
||||
case "dark", "black", "night", "latte":
|
||||
settings.IsThemeDark = true
|
||||
default:
|
||||
settings.IsThemeDark = false
|
||||
data := struct {
|
||||
LanguageOptions []LanguageOption
|
||||
CurrentLang string
|
||||
Theme string
|
||||
Safe string
|
||||
}{
|
||||
LanguageOptions: languageOptions,
|
||||
CurrentLang: settings.Language,
|
||||
Theme: settings.Theme,
|
||||
Safe: settings.SafeSearch,
|
||||
}
|
||||
|
||||
if query == "" {
|
||||
data := struct {
|
||||
LanguageOptions []LanguageOption
|
||||
CurrentLang string
|
||||
CurrentSearchLang string
|
||||
Theme string
|
||||
Safe string
|
||||
IsThemeDark bool
|
||||
}{
|
||||
LanguageOptions: languageOptions,
|
||||
CurrentLang: settings.SiteLanguage,
|
||||
CurrentSearchLang: settings.SearchLanguage,
|
||||
Theme: settings.Theme,
|
||||
Safe: settings.SafeSearch,
|
||||
IsThemeDark: settings.IsThemeDark,
|
||||
}
|
||||
|
||||
tmpl := template.Must(template.ParseFiles("templates/search.html"))
|
||||
tmpl.Execute(w, data)
|
||||
return
|
||||
}
|
||||
|
||||
settings := loadUserSettings(r)
|
||||
|
||||
// Handle search based on the type
|
||||
switch searchType {
|
||||
case "image":
|
||||
handleImageSearch(w, settings, query, page)
|
||||
|
@ -183,8 +167,7 @@ func runServer() {
|
|||
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
||||
http.HandleFunc("/", handleSearch)
|
||||
http.HandleFunc("/search", handleSearch)
|
||||
http.HandleFunc("/suggestions", handleSuggestions)
|
||||
http.HandleFunc("/imgproxy", handleImageProxy)
|
||||
http.HandleFunc("/img_proxy", handleImageProxy)
|
||||
http.HandleFunc("/node", handleNodeRequest)
|
||||
http.HandleFunc("/settings", handleSettings)
|
||||
http.HandleFunc("/save-settings", handleSaveSettings)
|
||||
|
|
|
@ -12,7 +12,7 @@ type OpenSearchDescription struct {
|
|||
ShortName string `xml:"ShortName"`
|
||||
Description string `xml:"Description"`
|
||||
Tags string `xml:"Tags"`
|
||||
URLs []URL `xml:"Url"`
|
||||
URL URL `xml:"Url"`
|
||||
}
|
||||
|
||||
type URL struct {
|
||||
|
@ -25,18 +25,12 @@ func generateOpenSearchXML(config Config) {
|
|||
|
||||
opensearch := OpenSearchDescription{
|
||||
Xmlns: "http://a9.com/-/spec/opensearch/1.1/",
|
||||
ShortName: "Warp",
|
||||
Description: "Warp search engine",
|
||||
Tags: "search, engine, warp",
|
||||
URLs: []URL{
|
||||
{
|
||||
Type: "text/html",
|
||||
Template: fmt.Sprintf("%s/search?q={searchTerms}", baseURL),
|
||||
},
|
||||
{
|
||||
Type: "application/x-suggestions+json",
|
||||
Template: fmt.Sprintf("%s/suggestions?q={searchTerms}", baseURL),
|
||||
},
|
||||
ShortName: "Search Engine",
|
||||
Description: "Search engine",
|
||||
Tags: "search, engine",
|
||||
URL: URL{
|
||||
Type: "text/html",
|
||||
Template: fmt.Sprintf("%s/search?q={searchTerms}", baseURL),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
|
||||
|
||||
.search-page-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-page-content h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#search-input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.search-type-icons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 30px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icon-button .material-icons-round {
|
||||
font-size: 48px;
|
||||
color: var(--sub-search-wrapper-ico);
|
||||
}
|
||||
|
||||
.icon-button p {
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--sub-search-wrapper-ico);
|
||||
}
|
||||
|
||||
.icon-button:hover .material-icons-round {
|
||||
transition: all .3s ease;
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.icon-button:hover p {
|
||||
transition: all .3s ease;
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.icon-button button:focus {
|
||||
outline: none;
|
||||
}
|
|
@ -53,48 +53,10 @@
|
|||
gap: 10px;
|
||||
}
|
||||
|
||||
#searchLanguageSelect,
|
||||
#safeSearchSelect,
|
||||
#siteLanguageSelect {
|
||||
border-radius: 4px;
|
||||
padding: 6px;
|
||||
font-size: 15px;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--font-fg);
|
||||
width: 160px;
|
||||
background: var(--button);
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-align: center;
|
||||
box-sizing: border-box; /* Ensures consistent width with padding */
|
||||
}
|
||||
|
||||
#searchLanguageSelect:hover,
|
||||
#safeSearchSelect:hover,
|
||||
#siteLanguageSelect:hover {
|
||||
border: 1px solid #5f6368;
|
||||
/* background-color: var(--button-hover); */
|
||||
}
|
||||
|
||||
.save.save-settings-page {
|
||||
padding: 6px;
|
||||
width: 160px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
/* Ensure correct aligment */
|
||||
.settings-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.settings-row select,
|
||||
.settings-row button {
|
||||
width: 160px;
|
||||
height: 40px;
|
||||
box-sizing: border-box;
|
||||
@media (max-width: 600px) {
|
||||
.theme-link {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- */
|
||||
|
|
|
@ -67,10 +67,13 @@
|
|||
/* Support for all WebKit browsers. */
|
||||
-webkit-font-feature-settings: 'liga';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
/* Support for Safari and Chrome. */
|
||||
text-rendering: optimizeLegibility;
|
||||
|
||||
/* Support for Firefox. */
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
/* Support for IE. */
|
||||
font-feature-settings: 'liga';
|
||||
}
|
||||
|
@ -546,9 +549,8 @@ hr {
|
|||
margin: 0 auto;
|
||||
background: var(--search-bg-input);
|
||||
border-radius: 22px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
position: absolute;
|
||||
width: 520px;
|
||||
overflow: hidden;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
@ -652,7 +654,7 @@ hr {
|
|||
|
||||
.settings-nav {
|
||||
max-width: 100%;
|
||||
height: 50px;
|
||||
height: 40px;
|
||||
background-color: var(--search-bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 10px;
|
||||
|
@ -672,7 +674,6 @@ hr {
|
|||
.settings-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 2px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
|
@ -681,9 +682,7 @@ hr {
|
|||
|
||||
.settings-row select,
|
||||
.settings-row button {
|
||||
width: 160px;
|
||||
height: 40px;
|
||||
margin: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.kno_wiki {
|
||||
|
@ -774,8 +773,6 @@ form.torrent-sort {
|
|||
|
||||
#settingsButton {
|
||||
transition: all .3s ease;
|
||||
/* width: 283px+6px;
|
||||
height: 31px; */
|
||||
}
|
||||
|
||||
.settings-icon-link {
|
||||
|
@ -827,12 +824,6 @@ form.torrent-sort {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-menu select:hover {
|
||||
border: 1px solid #5f6368;
|
||||
cursor: pointer;
|
||||
transition: all .3s ease;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -876,7 +867,7 @@ form.torrent-sort {
|
|||
|
||||
.theme-settings {
|
||||
margin-top: 10px;
|
||||
width: 100%-4px;
|
||||
width: 90%;
|
||||
border: 1px solid var(--snip-border);
|
||||
background: var(--snip-background);
|
||||
color: var(--fg);
|
||||
|
@ -890,10 +881,6 @@ form.torrent-sort {
|
|||
margin-left: 3%;
|
||||
}
|
||||
|
||||
.theme-mini-settings {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.settings-search-div:hover p {
|
||||
color: #8ab4f8;
|
||||
}
|
||||
|
@ -1171,7 +1158,7 @@ p {
|
|||
position: absolute;
|
||||
margin-top: 0px;
|
||||
top: 20px;
|
||||
left: 28px;
|
||||
left: 38px;
|
||||
}
|
||||
|
||||
.sub-search-button-wrapper button {
|
||||
|
@ -1553,20 +1540,6 @@ body, h1, p, a, input, button {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
.material-icons-round {
|
||||
font-family: 'Material Icons Round';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
line-height: 1;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
direction: ltr;
|
||||
} */
|
||||
|
||||
@media only screen and (max-width: 1220px) {
|
||||
|
||||
.snip {
|
||||
|
@ -1759,9 +1732,9 @@ body, h1, p, a, input, button {
|
|||
display: none;
|
||||
}
|
||||
|
||||
/* .search-menu {
|
||||
.search-menu {
|
||||
display: none;
|
||||
} */
|
||||
}
|
||||
|
||||
.sub-search-button-wrapper {
|
||||
margin: 0;
|
||||
|
@ -1865,9 +1838,9 @@ body, h1, p, a, input, button {
|
|||
top: 5px;
|
||||
}
|
||||
|
||||
/* .settings-search-div {
|
||||
.settings-search-div {
|
||||
display: none;
|
||||
} */
|
||||
}
|
||||
|
||||
.search-container h1 {
|
||||
font-size: 55px;
|
||||
|
@ -1888,19 +1861,9 @@ body, h1, p, a, input, button {
|
|||
}
|
||||
|
||||
.search-button-wrapper button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.icon-button button {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.icon-button p {
|
||||
margin-top: 30px;
|
||||
display: table-row;
|
||||
margin: 30px 0px 0px 0px;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
#clearSearch {
|
||||
|
@ -1909,14 +1872,6 @@ body, h1, p, a, input, button {
|
|||
|
||||
}
|
||||
|
||||
/* This is really bad */
|
||||
@media only screen and (max-width: 400px) {
|
||||
|
||||
.icon-button {
|
||||
padding: 5%
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensuring dark theme compliance */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.leaflet-control-locate,
|
||||
|
@ -1938,7 +1893,6 @@ body, h1, p, a, input, button {
|
|||
color: var(--link) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
:root {
|
||||
--background-color: #ffffff;
|
||||
|
|
|
@ -1,180 +0,0 @@
|
|||
/**
|
||||
* @source: ./script.js (originally from araa-search on Github)
|
||||
*
|
||||
* @licstart The following is the entire license notice for the
|
||||
* JavaScript code in this page.
|
||||
*
|
||||
* Copyright (C) 2023 Extravi
|
||||
*
|
||||
* The JavaScript code in this page is free software: you can
|
||||
* redistribute it and/or modify it under the terms of the GNU Affero
|
||||
* General Public License as published by the Free Software Foundation,
|
||||
* either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* The code is distributed WITHOUT ANY WARRANTY; without even the
|
||||
* implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU Affero General Public License for more details.
|
||||
*
|
||||
* As additional permission under GNU Affero General Public License
|
||||
* section 7, you may distribute non-source (e.g., minimized or compacted)
|
||||
* forms of that code without the copy of the GNU Affero General Public
|
||||
* License normally required by section 4, provided you include this
|
||||
* license notice and a URL through which recipients can access the
|
||||
* Corresponding Source.
|
||||
*
|
||||
* @licend The above is the entire license notice
|
||||
* for the JavaScript code in this page.
|
||||
*/
|
||||
|
||||
// Removes the 'Apply Settings' button for Javascript users,
|
||||
// since changing any of the elements causes the settings to apply
|
||||
// automatically.
|
||||
let resultsSave = document.querySelector(".results-save");
|
||||
if (resultsSave != null) {
|
||||
resultsSave.style.display = "none";
|
||||
}
|
||||
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const searchWrapper = document.querySelectorAll('.wrapper, .wrapper-results')[0];
|
||||
const resultsWrapper = document.querySelector('.autocomplete');
|
||||
// const clearSearch = document.querySelector("#clearSearch");
|
||||
|
||||
async function getSuggestions(query) {
|
||||
try {
|
||||
const params = new URLSearchParams({ "q": query }).toString();
|
||||
const response = await fetch(`/suggestions?${params}`);
|
||||
const data = await response.json();
|
||||
return data[1]; // Return only the array of suggestion strings
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
let currentIndex = -1; // Keep track of the currently selected suggestion
|
||||
|
||||
// Handle click events on the type buttons
|
||||
let results = [];
|
||||
searchInput.addEventListener('input', async () => {
|
||||
let input = searchInput.value;
|
||||
if (input.length) {
|
||||
results = await getSuggestions(input);
|
||||
}
|
||||
renderResults(results);
|
||||
currentIndex = -1; // Reset index when we return new results
|
||||
});
|
||||
|
||||
searchInput.addEventListener("focus", async () => {
|
||||
let input = searchInput.value;
|
||||
if (results.length === 0 && input.length != 0) {
|
||||
results = await getSuggestions(input);
|
||||
}
|
||||
renderResults(results);
|
||||
})
|
||||
|
||||
// clearSearch.style.visibility = "visible"; // Only show the clear search button for JS users.
|
||||
// clearSearch.addEventListener("click", () => {
|
||||
// searchInput.value = "";
|
||||
// searchInput.focus();
|
||||
// })
|
||||
|
||||
searchInput.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'ArrowUp' || event.key === 'ArrowDown' || event.key === 'Tab') {
|
||||
event.preventDefault(); // Prevent the default behavior, such as moving the cursor
|
||||
|
||||
// Find the currently selected suggestion element
|
||||
const selectedSuggestion = resultsWrapper.querySelector('.selected');
|
||||
if (selectedSuggestion) {
|
||||
selectedSuggestion.classList.remove('selected'); // Deselect the currently selected suggestion
|
||||
}
|
||||
|
||||
// Increment current index when ArrowUp is pressed otherwise hen Tab OR ArrowDown decrement
|
||||
if (event.key === 'ArrowUp') {
|
||||
currentIndex--;
|
||||
} else {
|
||||
currentIndex++;
|
||||
}
|
||||
|
||||
// Wrap around the index if it goes out of bounds
|
||||
if (currentIndex < 0) {
|
||||
currentIndex = resultsWrapper.querySelectorAll('li').length - 1;
|
||||
} else if (currentIndex >= resultsWrapper.querySelectorAll('li').length) {
|
||||
currentIndex = 0;
|
||||
}
|
||||
|
||||
// Select the new suggestion
|
||||
resultsWrapper.querySelectorAll('li')[currentIndex].classList.add('selected');
|
||||
// Update the value of the search input
|
||||
searchInput.value = resultsWrapper.querySelectorAll('li')[currentIndex].textContent;
|
||||
}
|
||||
});
|
||||
|
||||
// Default to the currently selected type or fallback to 'text'
|
||||
let selectedType = document.querySelector('.search-active')?.value || 'text';
|
||||
|
||||
// Function to render results
|
||||
function renderResults(results) {
|
||||
if (!results || !results.length || !searchInput.value) {
|
||||
searchWrapper.classList.remove('show');
|
||||
return;
|
||||
}
|
||||
|
||||
let content = '';
|
||||
results.forEach((item) => {
|
||||
content += `<li>${item}</li>`;
|
||||
});
|
||||
|
||||
if (searchInput.value) {
|
||||
searchWrapper.classList.add('show');
|
||||
}
|
||||
resultsWrapper.innerHTML = `<ul>${content}</ul>`;
|
||||
}
|
||||
|
||||
// Handle click events on the type buttons
|
||||
const typeButtons = document.querySelectorAll('[name="t"]');
|
||||
typeButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
selectedType = this.value;
|
||||
typeButtons.forEach(btn => btn.classList.remove('search-active'));
|
||||
this.classList.add('search-active');
|
||||
});
|
||||
});
|
||||
|
||||
// Handle clicks on search results
|
||||
resultsWrapper.addEventListener('click', (event) => {
|
||||
if (event.target.tagName === 'LI') {
|
||||
const query = event.target.textContent;
|
||||
window.location.href = `/search?q=${encodeURIComponent(query)}&t=${encodeURIComponent(selectedType)}`;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keypress", (event) => {
|
||||
if (document.activeElement == searchInput) {
|
||||
// Allow the '/' character to be pressed when searchInput is active
|
||||
} else if (document.querySelector(".calc") != null) {
|
||||
// Do nothing if the calculator is available, so the division keybinding
|
||||
// will still work
|
||||
}
|
||||
else if (event.key == "/") {
|
||||
event.preventDefault();
|
||||
searchInput.focus();
|
||||
searchInput.selectionStart = searchInput.selectionEnd = searchInput.value.length;
|
||||
}
|
||||
})
|
||||
|
||||
// Add event listener to hide autocomplete suggestions when clicking outside of search-input or wrapper
|
||||
document.addEventListener('click', (event) => {
|
||||
// Check if the target of the event is the search-input or any of its ancestors
|
||||
if (!searchInput.contains(event.target) && !searchWrapper.contains(event.target)) {
|
||||
// Remove the show class from the search wrapper
|
||||
searchWrapper.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
// // Update visual feedback for selected type on page load
|
||||
// document.addEventListener("DOMContentLoaded", () => {
|
||||
// const activeButton = document.querySelector(`[name="t"][value="${selectedType}"]`);
|
||||
// if (activeButton) {
|
||||
// typeButtons.forEach(btn => btn.classList.remove('search-active'));
|
||||
// activeButton.classList.add('search-active');
|
||||
// }
|
||||
// });
|
|
@ -1,96 +0,0 @@
|
|||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const templateData = document.getElementById('template-data');
|
||||
let page = parseInt(templateData.getAttribute('data-page')) || 1;
|
||||
const query = templateData.getAttribute('data-query') || '';
|
||||
let searchType = templateData.getAttribute('data-type') || 'text'; // Default to 'text' if not provided
|
||||
let loading = false;
|
||||
let hasMoreResults = true;
|
||||
const loadingIndicator = document.getElementById('message-bottom-left');
|
||||
let loadingTimeout;
|
||||
|
||||
function loadResults(newPage) {
|
||||
if (loading || !hasMoreResults) return;
|
||||
loading = true;
|
||||
|
||||
// Show loading indicator if taking more than 100ms
|
||||
loadingTimeout = setTimeout(() => {
|
||||
loadingIndicator.style.display = 'flex';
|
||||
}, 100);
|
||||
|
||||
fetch(`/search?q=${encodeURIComponent(query)}&t=${encodeURIComponent(searchType)}&p=${newPage}`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then(data => {
|
||||
clearTimeout(loadingTimeout);
|
||||
loadingIndicator.style.display = 'none';
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(data, 'text/html');
|
||||
const newResults = doc.getElementById('results').innerHTML;
|
||||
const noResultsMessage = `No results found for '${query}'. Try different keywords.`;
|
||||
const endOfResultsMessage = "Looks like this is the end of results.";
|
||||
const serverError = "Internal Server Error";
|
||||
|
||||
if (newResults.includes(noResultsMessage) || newResults.includes(endOfResultsMessage) || newResults.includes(serverError)) {
|
||||
document.getElementById('results').innerHTML += newResults;
|
||||
hasMoreResults = false;
|
||||
} else {
|
||||
document.getElementById('results').innerHTML += newResults;
|
||||
page = newPage;
|
||||
// Automatically load more results if content height is less than window height
|
||||
checkIfMoreResultsNeeded();
|
||||
}
|
||||
loading = false;
|
||||
})
|
||||
.catch(error => {
|
||||
clearTimeout(loadingTimeout);
|
||||
loadingIndicator.style.display = 'none';
|
||||
console.error('Error loading results:', error);
|
||||
hasMoreResults = false;
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
function checkIfMoreResultsNeeded() {
|
||||
if (document.body.scrollHeight <= window.innerHeight && hasMoreResults) {
|
||||
loadResults(page + 1);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
|
||||
loadResults(page + 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure the correct search button has the active class at load
|
||||
const buttons = document.querySelectorAll('.search-container-results-btn button');
|
||||
if (buttons.length === 0) {
|
||||
console.error("No search buttons found");
|
||||
} else {
|
||||
buttons.forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const activeElement = document.querySelector('.search-container-results-btn .search-active');
|
||||
if (activeElement) {
|
||||
activeElement.classList.remove('search-active');
|
||||
}
|
||||
this.classList.add('search-active');
|
||||
// Update search type when button is clicked
|
||||
searchType = this.getAttribute('value');
|
||||
});
|
||||
});
|
||||
|
||||
// Ensure one button is active on page load
|
||||
const initialActiveElement = document.querySelector('.search-container-results-btn .search-active');
|
||||
if (!initialActiveElement) {
|
||||
buttons[0].classList.add('search-active');
|
||||
searchType = buttons[0].getAttribute('value'); // Set default search type
|
||||
}
|
||||
}
|
||||
|
||||
// Check if more results are needed right after the initial content is loaded
|
||||
checkIfMoreResultsNeeded();
|
||||
});
|
286
suggestions.go
286
suggestions.go
|
@ -1,286 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SuggestionSource represents a search suggestion source along with its latency.
|
||||
type SuggestionSource struct {
|
||||
Name string
|
||||
FetchFunc func(string) []string
|
||||
Latency time.Duration
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// Initialize suggestion sources with default latency values.
|
||||
var suggestionSources = []SuggestionSource{
|
||||
{
|
||||
Name: "DuckDuckGo",
|
||||
FetchFunc: fetchDuckDuckGoSuggestions,
|
||||
Latency: 50 * time.Millisecond,
|
||||
},
|
||||
{
|
||||
Name: "Edge",
|
||||
FetchFunc: fetchEdgeSuggestions,
|
||||
Latency: 50 * time.Millisecond,
|
||||
},
|
||||
{
|
||||
Name: "Brave",
|
||||
FetchFunc: fetchBraveSuggestions,
|
||||
Latency: 50 * time.Millisecond,
|
||||
},
|
||||
{
|
||||
Name: "Ecosia",
|
||||
FetchFunc: fetchEcosiaSuggestions,
|
||||
Latency: 50 * time.Millisecond,
|
||||
},
|
||||
// { // Not working with fetchSuggestionsFromURL func
|
||||
// Name: "Qwant",
|
||||
// FetchFunc: fetchQwantSuggestions,
|
||||
// Latency: 50 * time.Millisecond,
|
||||
// },
|
||||
{
|
||||
Name: "Startpage",
|
||||
FetchFunc: fetchStartpageSuggestions,
|
||||
Latency: 50 * time.Millisecond,
|
||||
},
|
||||
{
|
||||
Name: "Yahoo",
|
||||
FetchFunc: fetchYahooSuggestions,
|
||||
Latency: 50 * time.Millisecond,
|
||||
},
|
||||
// I advise against it, but you can use it if you want to
|
||||
// {
|
||||
// Name: "Google",
|
||||
// FetchFunc: fetchGoogleSuggestions,
|
||||
// Latency: 500 * time.Millisecond,
|
||||
// },
|
||||
}
|
||||
|
||||
// Mutex to protect the suggestionSources during sorting.
|
||||
var suggestionsMU sync.Mutex
|
||||
|
||||
func handleSuggestions(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query().Get("q")
|
||||
if query == "" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `["",[]]`)
|
||||
return
|
||||
}
|
||||
|
||||
// Sort the suggestion sources based on their latency.
|
||||
suggestionsMU.Lock()
|
||||
sort.Slice(suggestionSources, func(i, j int) bool {
|
||||
return suggestionSources[i].Latency < suggestionSources[j].Latency
|
||||
})
|
||||
suggestionsMU.Unlock()
|
||||
|
||||
var suggestions []string
|
||||
for i := range suggestionSources {
|
||||
source := &suggestionSources[i]
|
||||
start := time.Now()
|
||||
suggestions = source.FetchFunc(query)
|
||||
elapsed := time.Since(start)
|
||||
|
||||
updateLatency(source, elapsed)
|
||||
|
||||
if len(suggestions) > 0 {
|
||||
printDebug("Suggestions found using %s", source.Name)
|
||||
break
|
||||
} else {
|
||||
printWarn("%s did not return any suggestions or failed.", source.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Trim the suggestions to a maximum of 8 items
|
||||
suggestions = trimSuggestions(suggestions)
|
||||
|
||||
if len(suggestions) == 0 {
|
||||
printErr("All suggestion services failed. Returning empty response.")
|
||||
}
|
||||
|
||||
// Return the final suggestions as JSON.
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `["",%s]`, toJSONStringArray(suggestions))
|
||||
}
|
||||
|
||||
// trimSuggestions trims the suggestion list to a maximum of 8 suggestions.
|
||||
func trimSuggestions(suggestions []string) []string {
|
||||
if len(suggestions) > 8 {
|
||||
return suggestions[:8]
|
||||
}
|
||||
return suggestions
|
||||
}
|
||||
|
||||
// updateLatency updates the latency of a suggestion source using an exponential moving average.
|
||||
func updateLatency(source *SuggestionSource, newLatency time.Duration) {
|
||||
source.mu.Lock()
|
||||
defer source.mu.Unlock()
|
||||
const alpha = 0.5 // Smoothing factor.
|
||||
source.Latency = time.Duration(float64(source.Latency)*(1-alpha) + float64(newLatency)*alpha)
|
||||
}
|
||||
|
||||
func fetchGoogleSuggestions(query string) []string {
|
||||
encodedQuery := url.QueryEscape(query)
|
||||
url := fmt.Sprintf("http://suggestqueries.google.com/complete/search?client=firefox&q=%s", encodedQuery)
|
||||
printDebug("Fetching suggestions from Google: %s", url)
|
||||
return fetchSuggestionsFromURL(url)
|
||||
}
|
||||
|
||||
func fetchDuckDuckGoSuggestions(query string) []string {
|
||||
encodedQuery := url.QueryEscape(query)
|
||||
url := fmt.Sprintf("https://duckduckgo.com/ac/?q=%s&type=list", encodedQuery)
|
||||
printDebug("Fetching suggestions from DuckDuckGo: %s", url)
|
||||
return fetchSuggestionsFromURL(url)
|
||||
}
|
||||
|
||||
func fetchEdgeSuggestions(query string) []string {
|
||||
encodedQuery := url.QueryEscape(query)
|
||||
url := fmt.Sprintf("https://api.bing.com/osjson.aspx?query=%s", encodedQuery)
|
||||
printDebug("Fetching suggestions from Edge (Bing): %s", url)
|
||||
return fetchSuggestionsFromURL(url)
|
||||
}
|
||||
|
||||
func fetchBraveSuggestions(query string) []string {
|
||||
encodedQuery := url.QueryEscape(query)
|
||||
url := fmt.Sprintf("https://search.brave.com/api/suggest?q=%s", encodedQuery)
|
||||
printDebug("Fetching suggestions from Brave: %s", url)
|
||||
return fetchSuggestionsFromURL(url)
|
||||
}
|
||||
|
||||
func fetchEcosiaSuggestions(query string) []string {
|
||||
encodedQuery := url.QueryEscape(query)
|
||||
url := fmt.Sprintf("https://ac.ecosia.org/?q=%s&type=list", encodedQuery)
|
||||
printDebug("Fetching suggestions from Ecosia: %s", url)
|
||||
return fetchSuggestionsFromURL(url)
|
||||
}
|
||||
|
||||
// Is this working?
|
||||
func fetchQwantSuggestions(query string) []string {
|
||||
encodedQuery := url.QueryEscape(query)
|
||||
url := fmt.Sprintf("https://api.qwant.com/v3/suggest?q=%s", encodedQuery)
|
||||
printDebug("Fetching suggestions from Qwant: %s", url)
|
||||
return fetchSuggestionsFromURL(url)
|
||||
}
|
||||
|
||||
func fetchStartpageSuggestions(query string) []string {
|
||||
encodedQuery := url.QueryEscape(query)
|
||||
url := fmt.Sprintf("https://startpage.com/suggestions?q=%s", encodedQuery)
|
||||
printDebug("Fetching suggestions from Startpage: %s", url)
|
||||
return fetchSuggestionsFromURL(url)
|
||||
}
|
||||
|
||||
func fetchYahooSuggestions(query string) []string {
|
||||
encodedQuery := url.QueryEscape(query)
|
||||
url := fmt.Sprintf("https://search.yahoo.com/sugg/gossip/gossip-us-ura/?output=fxjson&command=%s", encodedQuery)
|
||||
printDebug("Fetching suggestions from Yahoo: %s", url)
|
||||
return fetchSuggestionsFromURL(url)
|
||||
}
|
||||
|
||||
// func fetchBaiduSuggestions(query string) []string {
|
||||
// encodedQuery := url.QueryEscape(query)
|
||||
// url := fmt.Sprintf("https://suggestion.baidu.com/su?wd=%s", encodedQuery)
|
||||
// printDebug("Fetching suggestions from Baidu: %s", url)
|
||||
// return fetchSuggestionsFromURL(url)
|
||||
// }
|
||||
|
||||
func fetchSuggestionsFromURL(url string) []string {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
printWarn("Error fetching suggestions from %s: %v", url, err)
|
||||
return []string{}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
printWarn("Error reading response body from %s: %v", url, err)
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// Print the raw HTTP response for debugging
|
||||
fmt.Printf("Raw response from %s:\n%s\n", url, string(body))
|
||||
|
||||
// Log the Content-Type for debugging.
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
printDebug("Response Content-Type from %s: %s", url, contentType)
|
||||
|
||||
// Check if the body is non-empty.
|
||||
if len(body) == 0 {
|
||||
printWarn("Received empty response body from %s", url)
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// Attempt to parse the response as JSON regardless of Content-Type.
|
||||
var parsedResponse []interface{}
|
||||
if err := json.Unmarshal(body, &parsedResponse); err != nil {
|
||||
printErr("Error parsing JSON from %s: %v", url, err)
|
||||
printDebug("Response body: %s", string(body))
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// Ensure the response structure is as expected.
|
||||
if len(parsedResponse) < 2 {
|
||||
printWarn("Unexpected response format from %v: %v", url, string(body))
|
||||
return []string{}
|
||||
}
|
||||
|
||||
suggestions := []string{}
|
||||
if items, ok := parsedResponse[1].([]interface{}); ok {
|
||||
for _, item := range items {
|
||||
if suggestion, ok := item.(string); ok {
|
||||
suggestions = append(suggestions, suggestion)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
printErr("Unexpected suggestions format in response from: %v", url)
|
||||
}
|
||||
|
||||
return suggestions
|
||||
}
|
||||
|
||||
// toJSONStringArray converts a slice of strings to a JSON array string.
|
||||
func toJSONStringArray(strings []string) string {
|
||||
result := ""
|
||||
for i, str := range strings {
|
||||
result += fmt.Sprintf(`"%s"`, str)
|
||||
if i < len(strings)-1 {
|
||||
result += ","
|
||||
}
|
||||
}
|
||||
return "[" + result + "]"
|
||||
}
|
||||
|
||||
// func testSuggestionSources(query string) {
|
||||
// for _, source := range suggestionSources {
|
||||
// fmt.Printf("Testing %s...\n", source.Name)
|
||||
|
||||
// // Fetch suggestions
|
||||
// suggestions := source.FetchFunc(query)
|
||||
|
||||
// // If we get results, print them
|
||||
// if len(suggestions) > 0 {
|
||||
// fmt.Printf("Suggestions from %s:\n", source.Name)
|
||||
// for i, suggestion := range suggestions {
|
||||
// fmt.Printf("%d: %s\n", i+1, suggestion)
|
||||
// }
|
||||
// } else {
|
||||
// fmt.Printf("No suggestions from %s.\n", source.Name)
|
||||
// }
|
||||
|
||||
// // Small separator for clarity
|
||||
// fmt.Println("--------------------------")
|
||||
// }
|
||||
// }
|
||||
|
||||
// func main() {
|
||||
// query := "test query"
|
||||
// testSuggestionSources(query)
|
||||
// }
|
|
@ -3,9 +3,6 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{{ if .IsThemeDark }}
|
||||
<meta name="darkreader-lock">
|
||||
{{ end }}
|
||||
<title>{{.Query}} - Ocásek</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
||||
|
@ -15,12 +12,8 @@
|
|||
<form action="/search" id="prev-next-form" class="results-search-container" method="GET" autocomplete="off">
|
||||
<h1 class="logomobile"><a class="no-decoration" href="./">Ocásek</a></h1>
|
||||
<div class="wrapper-results">
|
||||
<input type="text" name="q" value="{{ .Query }}" id="search-input"/>
|
||||
<input type="text" name="q" value="{{ .Query }}" id="search-input" placeholder="Type to search..." />
|
||||
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="file">search</button>
|
||||
<div class="autocomplete">
|
||||
<ul>
|
||||
</ul>
|
||||
</div>
|
||||
<input type="submit" class="hide" name="t" value="file" />
|
||||
</div>
|
||||
<div class="sub-search-button-wrapper">
|
||||
|
@ -112,7 +105,6 @@
|
|||
Try rephrasing your search term and/or recorrect any spelling mistakes.
|
||||
</div>
|
||||
{{ end }}
|
||||
<script defer src="/static/js/autocomplete.js"></script>
|
||||
<script>
|
||||
// Check if JavaScript is enabled and modify the DOM accordingly
|
||||
document.getElementById('content').classList.remove('js-enabled');
|
||||
|
|
|
@ -3,9 +3,6 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{{ if .IsThemeDark }}
|
||||
<meta name="darkreader-lock">
|
||||
{{ end }}
|
||||
<title>{{.Query}} - Ocásek</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
||||
|
@ -15,11 +12,8 @@
|
|||
<form action="/search" id="prev-next-form" class="results-search-container" method="GET" autocomplete="off">
|
||||
<h1 class="logomobile"><a class="no-decoration" href="./">Ocásek</a></h1>
|
||||
<div class="wrapper-results">
|
||||
<input type="text" name="q" value="{{ .Query }}" id="search-input"/>
|
||||
<input type="text" name="q" value="{{ .Query }}" id="search-input" placeholder="Type to search..." />
|
||||
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="forum">search</button>
|
||||
<div class="autocomplete">
|
||||
<ul></ul>
|
||||
</div>
|
||||
<input type="submit" class="hide" name="t" value="forum" />
|
||||
</div>
|
||||
<div class="sub-search-button-wrapper">
|
||||
|
@ -40,10 +34,10 @@
|
|||
<button name="t" value="forum" class="clickable search-active">Forums</button>
|
||||
</div>
|
||||
<div id="content" class="js-enabled">
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="map">map</button>
|
||||
<button name="t" value="map" class="clickable">Maps</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="map">map</button>
|
||||
<button name="t" value="map" class="clickable">Maps</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="file">share</button>
|
||||
|
@ -62,9 +56,9 @@
|
|||
<option value="{{.Code}}" {{if eq .Code $.CurrentLang}}selected{{end}}>{{.Name}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
<button class="results-save" name="t" value="forum">Apply settings</button>
|
||||
<button class="results-save" name="t" value="text">Apply settings</button>
|
||||
</form>
|
||||
<div class="results" id="results">
|
||||
<div class="results">
|
||||
{{if .Results}}
|
||||
{{range .Results}}
|
||||
<div class="result_item">
|
||||
|
@ -74,33 +68,22 @@
|
|||
</div>
|
||||
<br>
|
||||
{{end}}
|
||||
{{else if .NoResults}}
|
||||
<div class="no-results">No results found for '{{ .Query }}'. Try different keywords.</div>
|
||||
{{else}}
|
||||
<div class="no-more-results">Looks like this is the end of results.</div>
|
||||
<div class="no-results">No results found for '{{ .Query }}'. Try different keywords.</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="message-bottom-left" id="message-bottom-left">
|
||||
<span>Searching for new results...</span>
|
||||
</div>
|
||||
<div class="prev-next prev-img" id="prev-next">
|
||||
<div class="prev-next prev-img">
|
||||
<form action="/search" method="get">
|
||||
<input type="hidden" name="q" value="{{ .Query }}">
|
||||
<input type="hidden" name="t" value="forum">
|
||||
<div id="content" class="js-enabled">
|
||||
{{ if .HasPrevPage }}
|
||||
<button type="submit" name="p" value="{{ sub .Page 1 }}">Previous</button>
|
||||
{{ end }}
|
||||
{{ if .HasNextPage }}
|
||||
<button type="submit" name="p" value="{{ add .Page 1 }}">Next</button>
|
||||
{{ end }}
|
||||
</div>
|
||||
<input type="hidden" name="t" value="text">
|
||||
{{ if .HasPrevPage }}
|
||||
<button type="submit" name="p" value="{{ sub .Page 1 }}">Previous</button>
|
||||
{{ end }}
|
||||
{{ if .HasNextPage }}
|
||||
<button type="submit" name="p" value="{{ add .Page 1 }}">Next</button>
|
||||
{{ end }}
|
||||
</form>
|
||||
</div>
|
||||
<!-- Pass the relevant data for dynamic scrolling -->
|
||||
<div id="template-data" data-page="{{ .Page }}" data-query="{{ .Query }}" data-type="forum"></div>
|
||||
<script defer src="/static/js/dynamicscrolling.js"></script>
|
||||
<script defer src="/static/js/autocomplete.js"></script>
|
||||
<script>
|
||||
// Check if JavaScript is enabled and modify the DOM accordingly
|
||||
document.getElementById('content').classList.remove('js-enabled');
|
||||
|
|
|
@ -3,9 +3,6 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{{ if .IsThemeDark }}
|
||||
<meta name="darkreader-lock">
|
||||
{{ end }}
|
||||
<title>{{.Query}} - Ocásek</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
||||
|
@ -15,12 +12,8 @@
|
|||
<form action="/search" id="prev-next-form" class="results-search-container" method="GET" autocomplete="off">
|
||||
<h1 class="logomobile"><a class="no-decoration" href="./">Ocásek</a></h1>
|
||||
<div class="wrapper-results">
|
||||
<input type="text" name="q" value="{{ .Query }}" id="search-input"/>
|
||||
<input type="text" name="q" value="{{ .Query }}" id="search-input" placeholder="Type to search..." />
|
||||
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="image">search</button>
|
||||
<div class="autocomplete">
|
||||
<ul>
|
||||
</ul>
|
||||
</div>
|
||||
<input type="submit" class="hide" name="t" value="image" />
|
||||
</div>
|
||||
<div class="sub-search-button-wrapper">
|
||||
|
@ -108,12 +101,69 @@
|
|||
<div class="message-bottom-left" id="message-bottom-left">
|
||||
<span>Searching for new results...</span>
|
||||
</div>
|
||||
<div id="template-data" data-page="{{ .Page }}" data-query="{{ .Query }}" data-type="image"></div>
|
||||
<script defer src="/static/js/dynamicscrolling.js"></script>
|
||||
<script defer src="/static/js/autocomplete.js"></script>
|
||||
<script>
|
||||
// Check if JavaScript is enabled and modify the DOM accordingly
|
||||
document.getElementById('content').classList.remove('js-enabled');
|
||||
</script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
let page = {{ .Page }};
|
||||
const query = "{{ .Query }}";
|
||||
let loading = false;
|
||||
let hasMoreResults = true;
|
||||
const loadingIndicator = document.getElementById('message-bottom-left');
|
||||
let loadingTimeout;
|
||||
|
||||
function loadResults(newPage) {
|
||||
if (loading || !hasMoreResults) return;
|
||||
loading = true;
|
||||
|
||||
// Show loading indicator if taking more than 100ms
|
||||
loadingTimeout = setTimeout(() => {
|
||||
loadingIndicator.style.display = 'flex';
|
||||
}, 100);
|
||||
|
||||
fetch(`/search?q=${encodeURIComponent(query)}&t=image&p=${newPage}`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then(data => {
|
||||
clearTimeout(loadingTimeout);
|
||||
loadingIndicator.style.display = 'none';
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(data, 'text/html');
|
||||
const newResults = doc.getElementById('results').innerHTML;
|
||||
const noResultsMessage = "No results found for '{{ .Query }}'. Try different keywords.";
|
||||
const endOfResultsMessage = "Looks like this is the end of results.";
|
||||
const serverError = "Internal Server Error";
|
||||
|
||||
if (newResults.includes(noResultsMessage) || newResults.includes(endOfResultsMessage) || newResults.includes(serverError)) {
|
||||
document.getElementById('results').innerHTML += newResults;
|
||||
hasMoreResults = false;
|
||||
} else {
|
||||
document.getElementById('results').innerHTML += newResults;
|
||||
page = newPage;
|
||||
}
|
||||
loading = false;
|
||||
})
|
||||
.catch(error => {
|
||||
clearTimeout(loadingTimeout);
|
||||
loadingIndicator.style.display = 'none';
|
||||
console.error('Error loading results:', error);
|
||||
hasMoreResults = false; // Stop further attempts
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
|
||||
loadResults(page + 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -3,9 +3,6 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{{ if .IsThemeDark }}
|
||||
<meta name="darkreader-lock">
|
||||
{{ end }}
|
||||
<title>{{ .Query }} - Ocásek</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
||||
|
@ -30,12 +27,8 @@
|
|||
<form action="/search" id="prev-next-form" class="results-search-container" method="GET" autocomplete="off">
|
||||
<h1 class="logomobile"><a class="no-decoration" href="./">Ocásek</a></h1>
|
||||
<div class="wrapper-results">
|
||||
<input type="text" name="q" value="{{ .Query }}" id="search-input"/>
|
||||
<input type="text" name="q" value="{{ .Query }}" id="search-input" placeholder="Type to search..." />
|
||||
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="map">search</button>
|
||||
<div class="autocomplete">
|
||||
<ul>
|
||||
</ul>
|
||||
</div>
|
||||
<input type="submit" class="hide" name="t" value="map" />
|
||||
</div>
|
||||
<div class="sub-search-button-wrapper">
|
||||
|
|
|
@ -3,17 +3,12 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{{ if .IsThemeDark }}
|
||||
<meta name="darkreader-lock">
|
||||
{{ end }}
|
||||
<title>Search with Ocásek</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<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="Ocásek" href="/opensearch.xml">
|
||||
</head>
|
||||
<body>
|
||||
<script defer src="/static/js/autocomplete.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Define the updateSettings function first
|
||||
|
@ -53,17 +48,9 @@
|
|||
});
|
||||
|
||||
// Event listener for Language Selection
|
||||
if (siteLanguageSelect) {
|
||||
siteLanguageSelect.addEventListener('change', function () {
|
||||
updateSettings('site_lang', this.value);
|
||||
});
|
||||
}
|
||||
|
||||
// if (searchLanguageSelect) {
|
||||
// searchLanguageSelect.addEventListener('change', function () {
|
||||
// updateSettings('search_lang', this.value);
|
||||
// });
|
||||
// }
|
||||
document.getElementById('languageSelect').addEventListener('change', function () {
|
||||
updateSettings('lang', this.value);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<div class="settings-search-div settings-search-div-search">
|
||||
|
@ -73,7 +60,7 @@
|
|||
<h2>Settings</h2>
|
||||
<div class="settings-content">
|
||||
<button id="settingsButton" onclick="window.location.href='/settings'">All settings</button> <!-- Well its unessesary to use js here but this menu will not work without js anyway -->
|
||||
<div class="theme-settings theme-mini-settings">
|
||||
<div class="theme-settings">
|
||||
<p><span class="highlight">Current theme: </span> <span id="theme_name">{{.Theme}}</span></p>
|
||||
<div class="themes-settings-menu">
|
||||
<div><img class="view-image-search clickable" id="dark_theme" alt="Dark Theme" src="/static/images/dark.webp"></div>
|
||||
|
@ -84,7 +71,7 @@
|
|||
<option value="disabled" {{if eq .Safe "disabled"}}selected{{end}}>Safe Search Off</option>
|
||||
<option value="active" {{if eq .Safe "active"}}selected{{end}}>Safe Search On</option>
|
||||
</select>
|
||||
<select class="lang" name="site_lang" id="siteLanguageSelect">
|
||||
<select class="lang" name="lang" id="languageSelect">
|
||||
{{range .LanguageOptions}}
|
||||
<option value="{{.Code}}" {{if eq .Code $.CurrentLang}}selected{{end}}>{{.Name}}</option>
|
||||
{{end}}
|
||||
|
@ -92,48 +79,16 @@
|
|||
</div>
|
||||
</div>
|
||||
<form action="/search" class="search-container" method="post" autocomplete="off">
|
||||
<div class="search-page-content">
|
||||
<h1>Ocásek</h1>
|
||||
<div class="wrapper">
|
||||
<input type="text" name="q" autofocus id="search-input"/> <!-- placeholder="Type to search..." -->
|
||||
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="web" type="submit">search</button>
|
||||
<div class="autocomplete">
|
||||
<ul></ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-type-icons">
|
||||
<input type="hidden" name="p" value="1">
|
||||
|
||||
<div class="icon-button">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="text">search</button>
|
||||
<p>Web</p>
|
||||
</div>
|
||||
|
||||
<div class="icon-button">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="image">image</button>
|
||||
<p>Images</p>
|
||||
</div>
|
||||
|
||||
<div class="icon-button">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="video">movie</button>
|
||||
<p>Videos</p>
|
||||
</div>
|
||||
|
||||
<div class="icon-button">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="forum">forum</button>
|
||||
<p>Forums</p>
|
||||
</div>
|
||||
|
||||
<div class="icon-button">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="map">map</button>
|
||||
<p>Maps</p>
|
||||
</div>
|
||||
|
||||
<div class="icon-button">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="file">share</button>
|
||||
<p>Torrents</p>
|
||||
</div>
|
||||
</div>
|
||||
<h1>Ocásek</h1>
|
||||
<div class="wrapper">
|
||||
<input type="text" name="q" autofocus id="search-input" placeholder="Type to search..." />
|
||||
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="text" type="submit">search</button>
|
||||
<!-- <a id="clearSearch" class="material-icons-round">close</a> -->
|
||||
</div>
|
||||
<div class="search-button-wrapper">
|
||||
<input type="hidden" name="p" value="1">
|
||||
<button name="t" value="text" type="submit">Search Text</button>
|
||||
<button name="t" value="image" type="submit">Search Images</button>
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
|
|
|
@ -3,9 +3,6 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{{ if .IsThemeDark }}
|
||||
<meta name="darkreader-lock">
|
||||
{{ end }}
|
||||
<title>Settings - Ocásek</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
||||
|
@ -40,7 +37,7 @@
|
|||
</a>
|
||||
<a href="/search?theme=night" class="theme-link">
|
||||
<div class="view-image-search clickable" id="night">
|
||||
<img src="/static/images/night.webp" alt="Night">
|
||||
<img src="/static/images/night.webp" alt="night">
|
||||
<div class="theme-tooltip">Night</div>
|
||||
</div>
|
||||
</a>
|
||||
|
@ -74,25 +71,16 @@
|
|||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<p>Site Language</p>
|
||||
<select class="results-settings" name="site_lang" id="siteLanguageSelect">
|
||||
<p>Preferred Language</p>
|
||||
<select class="results-settings" name="lang" id="languageSelect">
|
||||
{{range .LanguageOptions}}
|
||||
<option value="{{.Code}}" {{if eq .Code $.CurrentSiteLang}}selected{{end}}>{{.Name}}</option>
|
||||
<option value="{{.Code}}" {{if eq .Code $.CurrentLang}}selected{{end}}>{{.Name}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<p>Search Language</p>
|
||||
<select class="results-settings" name="search_lang" id="searchLanguageSelect">
|
||||
{{range .LanguageOptions}}
|
||||
<option value="{{.Code}}" {{if eq .Code $.CurrentSearchLang}}selected{{end}}>{{.Name}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<p class="font-hide"> </p>
|
||||
<div class="settings-row settings-row2">
|
||||
<p class="font-hide">|</p>
|
||||
<button class="save save-settings-page" type="submit">Save Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,9 +3,6 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{{ if .IsThemeDark }}
|
||||
<meta name="darkreader-lock">
|
||||
{{ end }}
|
||||
<title>{{.Query}} - Ocásek</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
||||
|
@ -15,12 +12,8 @@
|
|||
<form action="/search" id="prev-next-form" class="results-search-container" method="GET" autocomplete="off">
|
||||
<h1 class="logomobile"><a class="no-decoration" href="./">Ocásek</a></h1>
|
||||
<div class="wrapper-results">
|
||||
<input type="text" name="q" value="{{ .Query }}" id="search-input"/>
|
||||
<input type="text" name="q" value="{{ .Query }}" id="search-input" placeholder="Type to search..." />
|
||||
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="text">search</button>
|
||||
<div class="autocomplete">
|
||||
<ul>
|
||||
</ul>
|
||||
</div>
|
||||
<input type="submit" class="hide" name="t" value="text" />
|
||||
</div>
|
||||
<div class="sub-search-button-wrapper">
|
||||
|
@ -88,22 +81,73 @@
|
|||
<form action="/search" method="get">
|
||||
<input type="hidden" name="q" value="{{ .Query }}">
|
||||
<input type="hidden" name="t" value="text">
|
||||
<div id="content" class="js-enabled">
|
||||
{{ if .HasPrevPage }}
|
||||
<button type="submit" name="p" value="{{ sub .Page 1 }}">Previous</button>
|
||||
{{ end }}
|
||||
{{ if .HasNextPage }}
|
||||
<button type="submit" name="p" value="{{ add .Page 1 }}">Next</button>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ if .HasPrevPage }}
|
||||
<button type="submit" name="p" value="{{ sub .Page 1 }}">Previous</button>
|
||||
{{ end }}
|
||||
{{ if .HasNextPage }}
|
||||
<button type="submit" name="p" value="{{ add .Page 1 }}">Next</button>
|
||||
{{ end }}
|
||||
</form>
|
||||
</div>
|
||||
<div id="template-data" data-page="{{ .Page }}" data-query="{{ .Query }}" data-type="text"></div>
|
||||
<script defer src="/static/js/dynamicscrolling.js"></script>
|
||||
<script defer src="/static/js/autocomplete.js"></script>
|
||||
<script>
|
||||
// Check if JavaScript is enabled and modify the DOM accordingly
|
||||
document.getElementById('content').classList.remove('js-enabled');
|
||||
</script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
if (document.getElementById('prev-next')) {
|
||||
document.getElementById('prev-next').style.display = 'none';
|
||||
|
||||
let page = {{ .Page }};
|
||||
const query = "{{ .Query }}";
|
||||
let loading = false;
|
||||
let hasMoreResults = true;
|
||||
const loadingIndicator = document.getElementById('message-bottom-left');
|
||||
let loadingTimeout;
|
||||
|
||||
function loadResults(newPage) {
|
||||
if (loading || !hasMoreResults) return;
|
||||
loading = true;
|
||||
|
||||
// Show loading indicator if taking more than 100ms
|
||||
loadingTimeout = setTimeout(() => {
|
||||
loadingIndicator.style.display = 'flex';
|
||||
}, 100);
|
||||
|
||||
fetch(`/search?q=${encodeURIComponent(query)}&t=text&p=${newPage}`)
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
clearTimeout(loadingTimeout);
|
||||
loadingIndicator.style.display = 'none';
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(data, 'text/html');
|
||||
const newResults = doc.getElementById('results').innerHTML;
|
||||
const noResultsMessage = "No results found for '{{ .Query }}'. Try different keywords.";
|
||||
|
||||
if (newResults.includes(noResultsMessage)) {
|
||||
document.getElementById('results').innerHTML += "<div class='no-more-results'>Looks like this is the end of results.</div>";
|
||||
hasMoreResults = false;
|
||||
} else {
|
||||
document.getElementById('results').innerHTML += newResults;
|
||||
page = newPage;
|
||||
}
|
||||
loading = false;
|
||||
})
|
||||
.catch(error => {
|
||||
clearTimeout(loadingTimeout);
|
||||
loadingIndicator.style.display = 'none';
|
||||
console.error('Error loading results:', error);
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
|
||||
loadResults(page + 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -3,9 +3,6 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{{ if .IsThemeDark }}
|
||||
<meta name="darkreader-lock">
|
||||
{{ end }}
|
||||
<title>{{.Query}} - Ocásek</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
||||
|
@ -15,12 +12,8 @@
|
|||
<form action="/search" id="prev-next-form" class="results-search-container" method="GET" autocomplete="off">
|
||||
<h1 class="logomobile"><a class="no-decoration" href="./">Ocásek</a></h1>
|
||||
<div class="wrapper-results">
|
||||
<input type="text" name="q" value="{{ .Query }}" id="search-input"/>
|
||||
<input type="text" name="q" value="{{ .Query }}" id="search-input" placeholder="Type to search..." />
|
||||
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="video">search</button>
|
||||
<div class="autocomplete">
|
||||
<ul>
|
||||
</ul>
|
||||
</div>
|
||||
<input type="submit" class="hide" name="t" value="video" />
|
||||
</div>
|
||||
<div class="sub-search-button-wrapper">
|
||||
|
@ -89,7 +82,6 @@
|
|||
{{ end }}
|
||||
</form>
|
||||
</div>
|
||||
<script defer src="/static/js/autocomplete.js"></script>
|
||||
<script>
|
||||
// Check if JavaScript is enabled and modify the DOM accordingly
|
||||
document.getElementById('content').classList.remove('js-enabled');
|
||||
|
|
|
@ -66,23 +66,14 @@ func buildSearchURL(query, safe, lang string, page, resultsPerPage int) string {
|
|||
}
|
||||
|
||||
langParam := ""
|
||||
var glParam, uuleParam string
|
||||
|
||||
if lang != "" {
|
||||
// Use lang as the geolocation
|
||||
langParam = "&lr=lang_" + lang
|
||||
glParam = "&gl=" + lang
|
||||
uuleParam = ""
|
||||
} else {
|
||||
// Use random geolocation
|
||||
glParam, uuleParam = getRandomGeoLocation()
|
||||
langParam = "&lr=" + lang
|
||||
}
|
||||
|
||||
// Generate random geolocation
|
||||
glParam, uuleParam := getRandomGeoLocation()
|
||||
|
||||
startIndex := (page - 1) * resultsPerPage
|
||||
|
||||
printDebug(fmt.Sprintf("https://www.google.com/search?q=%s%s%s%s%s&start=%d",
|
||||
url.QueryEscape(query), safeParam, langParam, glParam, uuleParam, startIndex))
|
||||
|
||||
return fmt.Sprintf("https://www.google.com/search?q=%s%s%s%s%s&start=%d",
|
||||
url.QueryEscape(query), safeParam, langParam, glParam, uuleParam, startIndex)
|
||||
}
|
||||
|
|
14
text.go
14
text.go
|
@ -22,17 +22,17 @@ func init() {
|
|||
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)
|
||||
cacheKey := CacheKey{Query: query, Page: page, Safe: settings.SafeSearch == "true", Lang: settings.Language, Type: "text"}
|
||||
combinedResults := getTextResultsFromCacheOrFetch(cacheKey, query, settings.SafeSearch, settings.Language, page)
|
||||
|
||||
hasPrevPage := page > 1 // dupe
|
||||
|
||||
//displayResults(w, combinedResults, query, lang, time.Since(startTime).Seconds(), page, hasPrevPage, hasNextPage)
|
||||
|
||||
// Prefetch next and previous pages
|
||||
go prefetchPage(query, settings.SafeSearch, settings.SearchLanguage, page+1)
|
||||
go prefetchPage(query, settings.SafeSearch, settings.Language, page+1)
|
||||
if hasPrevPage {
|
||||
go prefetchPage(query, settings.SafeSearch, settings.SearchLanguage, page-1)
|
||||
go prefetchPage(query, settings.SafeSearch, settings.Language, page-1)
|
||||
}
|
||||
|
||||
elapsedTime := time.Since(startTime)
|
||||
|
@ -55,7 +55,6 @@ func HandleTextSearch(w http.ResponseWriter, settings UserSettings, query string
|
|||
CurrentLang string
|
||||
Theme string
|
||||
Safe string
|
||||
IsThemeDark bool
|
||||
}{
|
||||
Results: combinedResults,
|
||||
Query: query,
|
||||
|
@ -65,10 +64,9 @@ func HandleTextSearch(w http.ResponseWriter, settings UserSettings, query string
|
|||
HasNextPage: len(combinedResults) >= 50,
|
||||
NoResults: len(combinedResults) == 0,
|
||||
LanguageOptions: languageOptions,
|
||||
CurrentLang: settings.SearchLanguage,
|
||||
CurrentLang: settings.Language,
|
||||
Theme: settings.Theme,
|
||||
Safe: settings.SafeSearch,
|
||||
IsThemeDark: settings.IsThemeDark,
|
||||
}
|
||||
|
||||
err = tmpl.Execute(w, data)
|
||||
|
@ -116,7 +114,7 @@ func getTextResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string,
|
|||
}
|
||||
|
||||
func prefetchPage(query, safe, lang string, page int) {
|
||||
cacheKey := CacheKey{Query: query, Page: page, Safe: safe == "active", Lang: lang, Type: "text"}
|
||||
cacheKey := CacheKey{Query: query, Page: page, Safe: safe == "true", Lang: lang, Type: "text"}
|
||||
if _, exists := resultsCache.Get(cacheKey); !exists {
|
||||
printInfo("Page %d not cached, caching now...", page)
|
||||
pageResults := fetchTextResults(query, safe, lang, page)
|
||||
|
|
151
user-settings.go
151
user-settings.go
|
@ -3,101 +3,62 @@ package main
|
|||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserSettings struct {
|
||||
Theme string
|
||||
SiteLanguage string
|
||||
SearchLanguage string
|
||||
SafeSearch string
|
||||
IsThemeDark bool
|
||||
Theme string
|
||||
Language string
|
||||
SafeSearch string
|
||||
}
|
||||
|
||||
func loadUserSettings(w http.ResponseWriter, r *http.Request) UserSettings {
|
||||
func loadUserSettings(r *http.Request) UserSettings {
|
||||
var settings UserSettings
|
||||
saveRequired := false
|
||||
|
||||
// Load theme
|
||||
if cookie, err := r.Cookie("theme"); err == nil {
|
||||
settings.Theme = cookie.Value
|
||||
} else {
|
||||
settings.Theme = "dark"
|
||||
saveRequired = true
|
||||
settings.Theme = "dark" // Default theme
|
||||
}
|
||||
|
||||
// Determine if the selected theme is dark
|
||||
settings.IsThemeDark = settings.Theme == "dark" || settings.Theme == "night" || settings.Theme == "black" || settings.Theme == "latte"
|
||||
|
||||
// Load site language
|
||||
if cookie, err := r.Cookie("site_language"); err == nil {
|
||||
settings.SiteLanguage = cookie.Value
|
||||
// Load language
|
||||
if cookie, err := r.Cookie("language"); err == nil {
|
||||
settings.Language = cookie.Value
|
||||
} else {
|
||||
// If no site language is set, use Accept-Language or default to "en"
|
||||
acceptLang := r.Header.Get("Accept-Language")
|
||||
if acceptLang != "" {
|
||||
settings.SiteLanguage = normalizeLangCode(strings.Split(acceptLang, ",")[0])
|
||||
} else {
|
||||
settings.SiteLanguage = "en" // Default language
|
||||
}
|
||||
saveRequired = true
|
||||
}
|
||||
|
||||
// Load search language (can be empty)
|
||||
if cookie, err := r.Cookie("search_language"); err == nil {
|
||||
settings.SearchLanguage = cookie.Value
|
||||
settings.Language = "en" // Default language
|
||||
}
|
||||
|
||||
// Load safe search
|
||||
if cookie, err := r.Cookie("safe"); err == nil {
|
||||
settings.SafeSearch = cookie.Value
|
||||
} else {
|
||||
settings.SafeSearch = ""
|
||||
saveRequired = true
|
||||
}
|
||||
|
||||
if saveRequired {
|
||||
saveUserSettings(w, settings)
|
||||
settings.SafeSearch = "" // Default safe search off
|
||||
}
|
||||
|
||||
return settings
|
||||
}
|
||||
|
||||
func saveUserSettings(w http.ResponseWriter, settings UserSettings) {
|
||||
expiration := time.Now().Add(90 * 24 * time.Hour)
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "theme",
|
||||
Value: settings.Theme,
|
||||
Path: "/",
|
||||
Expires: expiration,
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
Secure: true, // Ensure cookie is sent over HTTPS only
|
||||
SameSite: http.SameSiteNoneMode, // Set SameSite to None
|
||||
})
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "site_language",
|
||||
Value: settings.SiteLanguage,
|
||||
Name: "language",
|
||||
Value: settings.Language,
|
||||
Path: "/",
|
||||
Expires: expiration,
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "search_language",
|
||||
Value: settings.SearchLanguage,
|
||||
Path: "/",
|
||||
Expires: expiration,
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
Secure: true, // Ensure cookie is sent over HTTPS only
|
||||
SameSite: http.SameSiteNoneMode, // Set SameSite to None
|
||||
})
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "safe",
|
||||
Value: settings.SafeSearch,
|
||||
Path: "/",
|
||||
Expires: expiration,
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
Secure: true, // Ensure cookie is sent over HTTPS only
|
||||
SameSite: http.SameSiteNoneMode, // Set SameSite to None
|
||||
})
|
||||
|
||||
printDebug("settings saved: %v", settings)
|
||||
|
@ -106,30 +67,15 @@ func saveUserSettings(w http.ResponseWriter, settings UserSettings) {
|
|||
func handleSaveSettings(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
// Load current settings
|
||||
settings := loadUserSettings(w, r)
|
||||
settings := loadUserSettings(r)
|
||||
|
||||
// Update only the settings that were submitted in the form
|
||||
if theme := r.FormValue("theme"); theme != "" {
|
||||
settings.Theme = theme
|
||||
}
|
||||
|
||||
// Update site language if provided
|
||||
if siteLang := r.FormValue("site_lang"); siteLang != "" {
|
||||
settings.SiteLanguage = siteLang
|
||||
} else {
|
||||
// If site_lang is empty, try to get from Accept-Language header
|
||||
acceptLang := r.Header.Get("Accept-Language")
|
||||
if acceptLang != "" {
|
||||
settings.SiteLanguage = strings.Split(acceptLang, ",")[0]
|
||||
}
|
||||
if lang := r.FormValue("lang"); lang != "" {
|
||||
settings.Language = lang
|
||||
}
|
||||
|
||||
// Update search language if provided
|
||||
if searchLang := r.FormValue("search_lang"); searchLang != "" {
|
||||
settings.SearchLanguage = searchLang
|
||||
}
|
||||
|
||||
// Update safe search if provided
|
||||
if safe := r.FormValue("safe"); safe != "" {
|
||||
settings.SafeSearch = safe
|
||||
}
|
||||
|
@ -144,22 +90,18 @@ func handleSaveSettings(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
func handleSettings(w http.ResponseWriter, r *http.Request) {
|
||||
// Load user settings
|
||||
settings = loadUserSettings(w, r)
|
||||
settings = loadUserSettings(r)
|
||||
|
||||
data := struct {
|
||||
LanguageOptions []LanguageOption
|
||||
CurrentSiteLang string
|
||||
CurrentSearchLang string
|
||||
Theme string
|
||||
Safe string
|
||||
IsThemeDark bool
|
||||
LanguageOptions []LanguageOption
|
||||
CurrentLang string
|
||||
Theme string
|
||||
Safe string
|
||||
}{
|
||||
LanguageOptions: languageOptions,
|
||||
CurrentSiteLang: settings.SiteLanguage,
|
||||
CurrentSearchLang: settings.SearchLanguage,
|
||||
Theme: settings.Theme,
|
||||
Safe: settings.SafeSearch,
|
||||
IsThemeDark: settings.IsThemeDark,
|
||||
LanguageOptions: languageOptions,
|
||||
CurrentLang: settings.Language,
|
||||
Theme: settings.Theme,
|
||||
Safe: settings.SafeSearch,
|
||||
}
|
||||
|
||||
printDebug("Rendering settings with data: %+v", data)
|
||||
|
@ -178,36 +120,3 @@ func handleSettings(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to normalize language codes
|
||||
func normalizeLangCode(lang string) string {
|
||||
lang = strings.ToLower(lang)
|
||||
|
||||
// First, check if the language code is already valid
|
||||
if isValidLangCode(lang) {
|
||||
return lang
|
||||
}
|
||||
|
||||
// Strip regional codes (e.g., en-US -> en)
|
||||
if strings.Contains(lang, "-") {
|
||||
lang = strings.Split(lang, "-")[0]
|
||||
}
|
||||
|
||||
// Re-check if the normalized version is valid
|
||||
if isValidLangCode(lang) {
|
||||
return lang
|
||||
}
|
||||
|
||||
// If the language is not recognized, default to "en"
|
||||
return "en"
|
||||
}
|
||||
|
||||
// Helper function to check if a language code exists in the language options
|
||||
func isValidLangCode(lang string) bool {
|
||||
for _, opt := range languageOptions {
|
||||
if opt.Code == lang {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
9
video.go
Normal file → Executable file
9
video.go
Normal file → Executable file
|
@ -151,10 +151,10 @@ func makeHTMLRequest(query, safe, lang string, page int) (*VideoAPIResponse, err
|
|||
func handleVideoSearch(w http.ResponseWriter, settings UserSettings, query string, page int) {
|
||||
start := time.Now()
|
||||
|
||||
results := fetchVideoResults(query, settings.SafeSearch, settings.SearchLanguage, page)
|
||||
results := fetchVideoResults(query, settings.SafeSearch, settings.Language, page)
|
||||
if len(results) == 0 {
|
||||
printWarn("No results from primary search, trying other nodes")
|
||||
results = tryOtherNodesForVideoSearch(query, settings.SafeSearch, settings.SearchLanguage, page, []string{hostID})
|
||||
results = tryOtherNodesForVideoSearch(query, settings.SafeSearch, settings.Language, page, []string{hostID})
|
||||
}
|
||||
|
||||
elapsed := time.Since(start)
|
||||
|
@ -173,10 +173,9 @@ func handleVideoSearch(w http.ResponseWriter, settings UserSettings, query strin
|
|||
"HasPrevPage": page > 1,
|
||||
"HasNextPage": len(results) > 0,
|
||||
"LanguageOptions": languageOptions,
|
||||
"CurrentLang": settings.SearchLanguage,
|
||||
"CurrentLang": settings.Language,
|
||||
"Theme": settings.Theme,
|
||||
"Safe": settings.SafeSearch,
|
||||
"IsThemeDark": settings.IsThemeDark,
|
||||
})
|
||||
if err != nil {
|
||||
printErr("Error executing template: %v", err)
|
||||
|
@ -207,7 +206,7 @@ func fetchVideoResults(query, safe, lang string, page int) []VideoResult {
|
|||
Views: formatViews(item.Views),
|
||||
Creator: item.UploaderName,
|
||||
Publisher: "Piped",
|
||||
Image: item.Thumbnail, //fmt.Sprintf("/img_proxy?url=%s", url.QueryEscape(item.Thumbnail)), // Using image proxy is not working, but its not needed here as piped is proxy anyway
|
||||
Image: fmt.Sprintf("/img_proxy?url=%s", url.QueryEscape(item.Thumbnail)),
|
||||
Duration: formatDuration(item.Duration),
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue