This commit is contained in:
partisan 2024-08-13 16:31:28 +02:00
parent 04fb0ca629
commit e056832321
16 changed files with 2275 additions and 2252 deletions

710
agent.go Normal file → Executable file
View file

@ -1,355 +1,355 @@
package main package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"math/rand" "math/rand"
"net/http" "net/http"
"sort" "sort"
"sync" "sync"
"time" "time"
) )
type BrowserVersion struct { type BrowserVersion struct {
Version string `json:"version"` Version string `json:"version"`
Global float64 `json:"global"` Global float64 `json:"global"`
} }
type BrowserData struct { type BrowserData struct {
Firefox []BrowserVersion `json:"firefox"` Firefox []BrowserVersion `json:"firefox"`
Chromium []BrowserVersion `json:"chrome"` Chromium []BrowserVersion `json:"chrome"`
} }
var ( var (
cache = struct { cache = struct {
sync.RWMutex sync.RWMutex
data map[string]string data map[string]string
}{ }{
data: make(map[string]string), data: make(map[string]string),
} }
browserCache = struct { browserCache = struct {
sync.RWMutex sync.RWMutex
data BrowserData data BrowserData
expires time.Time expires time.Time
}{ }{
expires: time.Now(), expires: time.Now(),
} }
) )
func fetchLatestBrowserVersions() (BrowserData, error) { func fetchLatestBrowserVersions() (BrowserData, error) {
url := "https://raw.githubusercontent.com/Fyrd/caniuse/master/fulldata-json/data-2.0.json" url := "https://raw.githubusercontent.com/Fyrd/caniuse/master/fulldata-json/data-2.0.json"
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
return BrowserData{}, err return BrowserData{}, err
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return BrowserData{}, err return BrowserData{}, err
} }
var rawData map[string]interface{} var rawData map[string]interface{}
if err := json.Unmarshal(body, &rawData); err != nil { if err := json.Unmarshal(body, &rawData); err != nil {
return BrowserData{}, err return BrowserData{}, err
} }
stats := rawData["agents"].(map[string]interface{}) stats := rawData["agents"].(map[string]interface{})
var data BrowserData var data BrowserData
if firefoxData, ok := stats["firefox"].(map[string]interface{}); ok { if firefoxData, ok := stats["firefox"].(map[string]interface{}); ok {
for version, usage := range firefoxData["usage_global"].(map[string]interface{}) { for version, usage := range firefoxData["usage_global"].(map[string]interface{}) {
data.Firefox = append(data.Firefox, BrowserVersion{ data.Firefox = append(data.Firefox, BrowserVersion{
Version: version, Version: version,
Global: usage.(float64), Global: usage.(float64),
}) })
} }
} }
if chromeData, ok := stats["chrome"].(map[string]interface{}); ok { if chromeData, ok := stats["chrome"].(map[string]interface{}); ok {
for version, usage := range chromeData["usage_global"].(map[string]interface{}) { for version, usage := range chromeData["usage_global"].(map[string]interface{}) {
data.Chromium = append(data.Chromium, BrowserVersion{ data.Chromium = append(data.Chromium, BrowserVersion{
Version: version, Version: version,
Global: usage.(float64), Global: usage.(float64),
}) })
} }
} }
return data, nil return data, nil
} }
func getLatestBrowserVersions() (BrowserData, error) { func getLatestBrowserVersions() (BrowserData, error) {
browserCache.RLock() browserCache.RLock()
if time.Now().Before(browserCache.expires) { if time.Now().Before(browserCache.expires) {
data := browserCache.data data := browserCache.data
browserCache.RUnlock() browserCache.RUnlock()
return data, nil return data, nil
} }
browserCache.RUnlock() browserCache.RUnlock()
data, err := fetchLatestBrowserVersions() data, err := fetchLatestBrowserVersions()
if err != nil { if err != nil {
return BrowserData{}, err return BrowserData{}, err
} }
browserCache.Lock() browserCache.Lock()
browserCache.data = data browserCache.data = data
browserCache.expires = time.Now().Add(24 * time.Hour) browserCache.expires = time.Now().Add(24 * time.Hour)
browserCache.Unlock() browserCache.Unlock()
return data, nil return data, nil
} }
func randomUserAgent() (string, error) { func randomUserAgent() (string, error) {
browsers, err := getLatestBrowserVersions() browsers, err := getLatestBrowserVersions()
if err != nil { if err != nil {
return "", err return "", err
} }
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
// Simulated browser usage statistics (in percentages) // Simulated browser usage statistics (in percentages)
usageStats := map[string]float64{ usageStats := map[string]float64{
"Firefox": 30.0, "Firefox": 30.0,
"Chromium": 70.0, "Chromium": 70.0,
} }
// Calculate the probabilities for the versions // Calculate the probabilities for the versions
probabilities := []float64{0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625, 0.0078125, 0.00390625} probabilities := []float64{0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625, 0.0078125, 0.00390625}
// Select a browser based on usage statistics // Select a browser based on usage statistics
browserType := "" browserType := ""
randVal := rand.Float64() * 100 randVal := rand.Float64() * 100
cumulative := 0.0 cumulative := 0.0
for browser, usage := range usageStats { for browser, usage := range usageStats {
cumulative += usage cumulative += usage
if randVal < cumulative { if randVal < cumulative {
browserType = browser browserType = browser
break break
} }
} }
var versions []BrowserVersion var versions []BrowserVersion
switch browserType { switch browserType {
case "Firefox": case "Firefox":
versions = browsers.Firefox versions = browsers.Firefox
case "Chromium": case "Chromium":
versions = browsers.Chromium versions = browsers.Chromium
} }
if len(versions) == 0 { if len(versions) == 0 {
return "", fmt.Errorf("no versions found for browser: %s", browserType) return "", fmt.Errorf("no versions found for browser: %s", browserType)
} }
// Sort versions by usage (descending order) // Sort versions by usage (descending order)
sort.Slice(versions, func(i, j int) bool { sort.Slice(versions, func(i, j int) bool {
return versions[i].Global > versions[j].Global return versions[i].Global > versions[j].Global
}) })
// Select a version based on the probabilities // Select a version based on the probabilities
version := "" version := ""
randVal = rand.Float64() randVal = rand.Float64()
cumulative = 0.0 cumulative = 0.0
for i, p := range probabilities { for i, p := range probabilities {
cumulative += p cumulative += p
if randVal < cumulative && i < len(versions) { if randVal < cumulative && i < len(versions) {
version = versions[i].Version version = versions[i].Version
break break
} }
} }
if version == "" { if version == "" {
version = versions[len(versions)-1].Version version = versions[len(versions)-1].Version
} }
// Generate the user agent string // Generate the user agent string
userAgent := generateUserAgent(browserType, version) userAgent := generateUserAgent(browserType, version)
return userAgent, nil return userAgent, nil
} }
func generateUserAgent(browser, version string) string { func generateUserAgent(browser, version string) string {
oses := []struct { oses := []struct {
os string os string
probability float64 probability float64
}{ }{
{"Windows NT 10.0; Win64; x64", 44.0}, {"Windows NT 10.0; Win64; x64", 44.0},
{"Windows NT 11.0; Win64; x64", 44.0}, {"Windows NT 11.0; Win64; x64", 44.0},
{"X11; Linux x86_64", 1.0}, {"X11; Linux x86_64", 1.0},
{"X11; Ubuntu; Linux x86_64", 1.0}, {"X11; Ubuntu; Linux x86_64", 1.0},
{"Macintosh; Intel Mac OS X 10_15_7", 10.0}, {"Macintosh; Intel Mac OS X 10_15_7", 10.0},
} }
// Select an OS based on probabilities // Select an OS based on probabilities
randVal := rand.Float64() * 100 randVal := rand.Float64() * 100
cumulative := 0.0 cumulative := 0.0
selectedOS := "" selectedOS := ""
for _, os := range oses { for _, os := range oses {
cumulative += os.probability cumulative += os.probability
if randVal < cumulative { if randVal < cumulative {
selectedOS = os.os selectedOS = os.os
break break
} }
} }
switch browser { switch browser {
case "Firefox": case "Firefox":
return fmt.Sprintf("Mozilla/5.0 (%s; rv:%s) Gecko/20100101 Firefox/%s", selectedOS, version, version) return fmt.Sprintf("Mozilla/5.0 (%s; rv:%s) Gecko/20100101 Firefox/%s", selectedOS, version, version)
case "Chromium": case "Chromium":
return fmt.Sprintf("Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", selectedOS, version) return fmt.Sprintf("Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", selectedOS, version)
} }
return "" return ""
} }
func updateCachedUserAgents(newVersions BrowserData) { func updateCachedUserAgents(newVersions BrowserData) {
cache.Lock() cache.Lock()
defer cache.Unlock() defer cache.Unlock()
for key, userAgent := range cache.data { for key, userAgent := range cache.data {
randVal := rand.Float64() randVal := rand.Float64()
if randVal < 0.5 { if randVal < 0.5 {
updatedUserAgent := updateUserAgentVersion(userAgent, newVersions) updatedUserAgent := updateUserAgentVersion(userAgent, newVersions)
cache.data[key] = updatedUserAgent cache.data[key] = updatedUserAgent
} }
} }
} }
func updateUserAgentVersion(userAgent string, newVersions BrowserData) string { func updateUserAgentVersion(userAgent string, newVersions BrowserData) string {
// Parse the current user agent to extract browser and version // Parse the current user agent to extract browser and version
var browserType, version string var browserType, version string
if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", &version); err == nil { if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", &version); err == nil {
browserType = "Chromium" browserType = "Chromium"
} else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", &version); err == nil { } else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", &version); err == nil {
browserType = "Chromium" browserType = "Chromium"
} else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", &version); err == nil { } else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", &version); err == nil {
browserType = "Chromium" browserType = "Chromium"
} else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", &version); err == nil { } else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", &version); err == nil {
browserType = "Chromium" browserType = "Chromium"
} else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", &version); err == nil { } else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", &version); err == nil {
browserType = "Chromium" browserType = "Chromium"
} else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:%s) Gecko/20100101 Firefox/%s", &version, &version); err == nil { } else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:%s) Gecko/20100101 Firefox/%s", &version, &version); err == nil {
browserType = "Firefox" browserType = "Firefox"
} else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (Windows NT 11.0; Win64; x64; rv:%s) Gecko/20100101 Firefox/%s", &version, &version); err == nil { } else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (Windows NT 11.0; Win64; x64; rv:%s) Gecko/20100101 Firefox/%s", &version, &version); err == nil {
browserType = "Firefox" browserType = "Firefox"
} else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (X11; Linux x86_64; rv:%s) Gecko/20100101 Firefox/%s", &version, &version); err == nil { } else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (X11; Linux x86_64; rv:%s) Gecko/20100101 Firefox/%s", &version, &version); err == nil {
browserType = "Firefox" browserType = "Firefox"
} else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:%s) Gecko/20100101 Firefox/%s", &version, &version); err == nil { } else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:%s) Gecko/20100101 Firefox/%s", &version, &version); err == nil {
browserType = "Firefox" browserType = "Firefox"
} else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7; rv:%s) Gecko/20100101 Firefox/%s", &version, &version); err == nil { } else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7; rv:%s) Gecko/20100101 Firefox/%s", &version, &version); err == nil {
browserType = "Firefox" browserType = "Firefox"
} }
// Get the latest version for the browser type // Get the latest version for the browser type
var latestVersion string var latestVersion string
if browserType == "Firefox" { if browserType == "Firefox" {
latestVersion = newVersions.Firefox[0].Version latestVersion = newVersions.Firefox[0].Version
} else if browserType == "Chromium" { } else if browserType == "Chromium" {
latestVersion = newVersions.Chromium[0].Version latestVersion = newVersions.Chromium[0].Version
} }
// Update the user agent string with the new version // Update the user agent string with the new version
return generateUserAgent(browserType, latestVersion) return generateUserAgent(browserType, latestVersion)
} }
func periodicUpdate() { func periodicUpdate() {
for { for {
// Sleep for a random interval between 1 and 2 days // Sleep for a random interval between 1 and 2 days
time.Sleep(time.Duration(24+rand.Intn(24)) * time.Hour) time.Sleep(time.Duration(24+rand.Intn(24)) * time.Hour)
// Fetch the latest browser versions // Fetch the latest browser versions
newVersions, err := fetchLatestBrowserVersions() newVersions, err := fetchLatestBrowserVersions()
if err != nil { if err != nil {
fmt.Println("Error fetching latest browser versions:", err) printWarn("Error fetching latest browser versions: %v", err)
continue continue
} }
// Update the browser version cache // Update the browser version cache
browserCache.Lock() browserCache.Lock()
browserCache.data = newVersions browserCache.data = newVersions
browserCache.expires = time.Now().Add(24 * time.Hour) browserCache.expires = time.Now().Add(24 * time.Hour)
browserCache.Unlock() browserCache.Unlock()
// Update the cached user agents // Update the cached user agents
updateCachedUserAgents(newVersions) updateCachedUserAgents(newVersions)
} }
} }
func GetUserAgent(cacheKey string) (string, error) { func GetUserAgent(cacheKey string) (string, error) {
cache.RLock() cache.RLock()
userAgent, found := cache.data[cacheKey] userAgent, found := cache.data[cacheKey]
cache.RUnlock() cache.RUnlock()
if found { if found {
return userAgent, nil return userAgent, nil
} }
userAgent, err := randomUserAgent() userAgent, err := randomUserAgent()
if err != nil { if err != nil {
return "", err return "", err
} }
cache.Lock() cache.Lock()
cache.data[cacheKey] = userAgent cache.data[cacheKey] = userAgent
cache.Unlock() cache.Unlock()
return userAgent, nil return userAgent, nil
} }
func GetNewUserAgent(cacheKey string) (string, error) { func GetNewUserAgent(cacheKey string) (string, error) {
userAgent, err := randomUserAgent() userAgent, err := randomUserAgent()
if err != nil { if err != nil {
return "", err return "", err
} }
cache.Lock() cache.Lock()
cache.data[cacheKey] = userAgent cache.data[cacheKey] = userAgent
cache.Unlock() cache.Unlock()
return userAgent, nil return userAgent, nil
} }
func init() { func init() {
go periodicUpdate() go periodicUpdate()
} }
// func main() { // func main() {
// go periodicUpdate() // not needed here // go periodicUpdate() // not needed here
// cacheKey := "image-search" // cacheKey := "image-search"
// userAgent, err := GetUserAgent(cacheKey) // userAgent, err := GetUserAgent(cacheKey)
// if err != nil { // if err != nil {
// fmt.Println("Error:", err) // fmt.Println("Error:", err)
// return // return
// } // }
// fmt.Println("Generated User Agent:", userAgent) // fmt.Println("Generated User Agent:", userAgent)
// // Request a new user agent for the same key // // Request a new user agent for the same key
// newUserAgent, err := GetNewUserAgent(cacheKey) // newUserAgent, err := GetNewUserAgent(cacheKey)
// if err != nil { // if err != nil {
// fmt.Println("Error:", err) // fmt.Println("Error:", err)
// return // return
// } // }
// fmt.Println("New User Agent:", newUserAgent) // fmt.Println("New User Agent:", newUserAgent)
// AcacheKey := "image-search" // AcacheKey := "image-search"
// AuserAgent, err := GetUserAgent(AcacheKey) // AuserAgent, err := GetUserAgent(AcacheKey)
// if err != nil { // if err != nil {
// fmt.Println("Error:", err) // fmt.Println("Error:", err)
// return // return
// } // }
// fmt.Println("Generated User Agent:", AuserAgent) // fmt.Println("Generated User Agent:", AuserAgent)
// DcacheKey := "image-search" // DcacheKey := "image-search"
// DuserAgent, err := GetUserAgent(DcacheKey) // DuserAgent, err := GetUserAgent(DcacheKey)
// if err != nil { // if err != nil {
// fmt.Println("Error:", err) // fmt.Println("Error:", err)
// return // return
// } // }
// fmt.Println("Generated User Agent:", DuserAgent) // fmt.Println("Generated User Agent:", DuserAgent)
// } // }

99
common.go Normal file → Executable file
View file

@ -1,50 +1,49 @@
package main package main
import ( import (
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"html/template" "html/template"
"log" "strings"
"strings" )
)
var (
var ( funcs = template.FuncMap{
funcs = template.FuncMap{ "sub": func(a, b int) int {
"sub": func(a, b int) int { return a - b
return a - b },
}, "add": func(a, b int) int {
"add": func(a, b int) int { return a + b
return a + b },
}, }
} )
)
func generateStrongRandomString(length int) string {
func generateStrongRandomString(length int) string { bytes := make([]byte, length)
bytes := make([]byte, length) _, err := rand.Read(bytes)
_, err := rand.Read(bytes) if err != nil {
if err != nil { printErr("Error generating random string: %v", err)
log.Fatalf("Error generating random string: %v", err) }
} return base64.URLEncoding.EncodeToString(bytes)[:length]
return base64.URLEncoding.EncodeToString(bytes)[:length] }
}
// Checks if the URL already includes a protocol
// Checks if the URL already includes a protocol func hasProtocol(url string) bool {
func hasProtocol(url string) bool { return strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://")
return strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") }
}
// Checks if the domain is a local address
// Checks if the domain is a local address func isLocalAddress(domain string) bool {
func isLocalAddress(domain string) bool { return domain == "localhost" || strings.HasPrefix(domain, "127.") || strings.HasPrefix(domain, "192.168.") || strings.HasPrefix(domain, "10.")
return domain == "localhost" || strings.HasPrefix(domain, "127.") || strings.HasPrefix(domain, "192.168.") || strings.HasPrefix(domain, "10.") }
}
// Ensures that HTTP or HTTPS is befor the adress if needed
// Ensures that HTTP or HTTPS is befor the adress if needed func addProtocol(domain string) string {
func addProtocol(domain string) string { if hasProtocol(domain) {
if hasProtocol(domain) { return domain
return domain }
} if isLocalAddress(domain) {
if isLocalAddress(domain) { return "http://" + domain
return "http://" + domain }
} return "https://" + domain
return "https://" + domain }
}

515
files.go Normal file → Executable file
View file

@ -1,258 +1,257 @@
package main package main
import ( import (
"fmt" "fmt"
"html/template" "html/template"
"log" "net/http"
"net/http" "net/url"
"net/url" "regexp"
"regexp" "sort"
"sort" "strconv"
"strconv" "strings"
"strings" "time"
"time" )
)
type Settings struct {
type Settings struct { UxLang string
UxLang string Safe string
Safe string }
}
type TorrentSite interface {
type TorrentSite interface { Name() string
Name() string Search(query string, category string) ([]TorrentResult, error)
Search(query string, category string) ([]TorrentResult, error) }
}
var (
var ( torrentGalaxy TorrentSite
torrentGalaxy TorrentSite nyaa TorrentSite
nyaa TorrentSite thePirateBay TorrentSite
thePirateBay TorrentSite rutor TorrentSite
rutor TorrentSite )
)
var fileResultsChan = make(chan []TorrentResult)
var fileResultsChan = make(chan []TorrentResult)
func initializeTorrentSites() {
func initializeTorrentSites() { torrentGalaxy = NewTorrentGalaxy()
torrentGalaxy = NewTorrentGalaxy() // nyaa = NewNyaa()
// nyaa = NewNyaa() thePirateBay = NewThePirateBay()
thePirateBay = NewThePirateBay() // rutor = NewRutor()
// rutor = NewRutor() }
}
func handleFileSearch(w http.ResponseWriter, settings UserSettings, query string, page int) {
func handleFileSearch(w http.ResponseWriter, settings UserSettings, query, safe, lang string, page int) { startTime := time.Now()
startTime := time.Now()
cacheKey := CacheKey{Query: query, Page: page, Safe: settings.SafeSearch == "true", Lang: settings.Language, Type: "file"}
cacheKey := CacheKey{Query: query, Page: page, Safe: safe == "true", Lang: lang, Type: "file"} combinedResults := getFileResultsFromCacheOrFetch(cacheKey, query, settings.SafeSearch, settings.Language, page)
combinedResults := getFileResultsFromCacheOrFetch(cacheKey, query, safe, lang, page)
sort.Slice(combinedResults, func(i, j int) bool { return combinedResults[i].Seeders > combinedResults[j].Seeders })
sort.Slice(combinedResults, func(i, j int) bool { return combinedResults[i].Seeders > combinedResults[j].Seeders })
elapsedTime := time.Since(startTime)
elapsedTime := time.Since(startTime) funcMap := template.FuncMap{
funcMap := template.FuncMap{ "sub": func(a, b int) int { return a - b },
"sub": func(a, b int) int { return a - b }, "add": func(a, b int) int { return a + b },
"add": func(a, b int) int { return a + b }, }
} tmpl, err := template.New("files.html").Funcs(funcMap).ParseFiles("templates/files.html")
tmpl, err := template.New("files.html").Funcs(funcMap).ParseFiles("templates/files.html") if err != nil {
if err != nil { printErr("Failed to load template: %v", err)
log.Printf("Failed to load template: %v", err) http.Error(w, "Failed to load template", http.StatusInternalServerError)
http.Error(w, "Failed to load template", http.StatusInternalServerError) return
return }
}
data := struct {
data := struct { Results []TorrentResult
Results []TorrentResult Query string
Query string Fetched string
Fetched string Category string
Category string Sort string
Sort string HasPrevPage bool
HasPrevPage bool HasNextPage bool
HasNextPage bool Page int
Page int Settings Settings
Settings Settings Theme string
Theme string }{
}{ Results: combinedResults,
Results: combinedResults, Query: query,
Query: query, Fetched: fmt.Sprintf("%.2f", elapsedTime.Seconds()),
Fetched: fmt.Sprintf("%.2f", elapsedTime.Seconds()), Category: "all",
Category: "all", Sort: "seed",
Sort: "seed", HasPrevPage: page > 1,
HasPrevPage: page > 1, HasNextPage: len(combinedResults) > 0,
HasNextPage: len(combinedResults) > 0, Page: page,
Page: page, Settings: Settings{UxLang: settings.Language, Safe: settings.SafeSearch}, // Now this is painful, are there two Settings variables??
Settings: Settings{UxLang: lang, Safe: safe}, // Now this is painful, are there two Settings variables?? Theme: settings.Theme,
Theme: settings.Theme, }
}
// // Debugging: Print results before rendering template
// // Debugging: Print results before rendering template // for _, result := range combinedResults {
// for _, result := range combinedResults { // fmt.Printf("Title: %s, Magnet: %s\n", result.Title, result.Magnet)
// fmt.Printf("Title: %s, Magnet: %s\n", result.Title, result.Magnet) // }
// }
if err := tmpl.Execute(w, data); err != nil {
if err := tmpl.Execute(w, data); err != nil { printErr("Failed to render template: %v", err)
log.Printf("Failed to render template: %v", err) http.Error(w, "Failed to render template", http.StatusInternalServerError)
http.Error(w, "Failed to render template", http.StatusInternalServerError) }
} }
}
func getFileResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string, page int) []TorrentResult {
func getFileResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string, page int) []TorrentResult { cacheChan := make(chan []SearchResult)
cacheChan := make(chan []SearchResult) var combinedResults []TorrentResult
var combinedResults []TorrentResult
go func() {
go func() { results, exists := resultsCache.Get(cacheKey)
results, exists := resultsCache.Get(cacheKey) if exists {
if exists { printInfo("Cache hit")
log.Println("Cache hit") cacheChan <- results
cacheChan <- results } else {
} else { printInfo("Cache miss")
log.Println("Cache miss") cacheChan <- nil
cacheChan <- nil }
} }()
}()
select {
select { case results := <-cacheChan:
case results := <-cacheChan: if results == nil {
if results == nil { combinedResults = fetchFileResults(query, safe, lang, page)
combinedResults = fetchFileResults(query, safe, lang, page) if len(combinedResults) > 0 {
if len(combinedResults) > 0 { resultsCache.Set(cacheKey, convertToSearchResults(combinedResults))
resultsCache.Set(cacheKey, convertToSearchResults(combinedResults)) }
} } else {
} else { _, torrentResults, _ := convertToSpecificResults(results)
_, torrentResults, _ := convertToSpecificResults(results) combinedResults = torrentResults
combinedResults = torrentResults }
} case <-time.After(2 * time.Second):
case <-time.After(2 * time.Second): printInfo("Cache check timeout")
log.Println("Cache check timeout") combinedResults = fetchFileResults(query, safe, lang, page)
combinedResults = fetchFileResults(query, safe, lang, page) if len(combinedResults) > 0 {
if len(combinedResults) > 0 { resultsCache.Set(cacheKey, convertToSearchResults(combinedResults))
resultsCache.Set(cacheKey, convertToSearchResults(combinedResults)) }
} }
}
return combinedResults
return combinedResults }
}
func fetchFileResults(query, safe, lang string, page int) []TorrentResult {
func fetchFileResults(query, safe, lang string, page int) []TorrentResult { sites := []TorrentSite{torrentGalaxy, nyaa, thePirateBay, rutor}
sites := []TorrentSite{torrentGalaxy, nyaa, thePirateBay, rutor} results := []TorrentResult{}
results := []TorrentResult{}
for _, site := range sites {
for _, site := range sites { if site == nil {
if site == nil { continue
continue }
} res, err := site.Search(query, "all")
res, err := site.Search(query, "all") if err != nil {
if err != nil { continue
continue }
} for _, r := range res {
for _, r := range res { r.Magnet = removeMagnetLink(r.Magnet) // Remove "magnet:", prehaps usless now?
r.Magnet = removeMagnetLink(r.Magnet) // Remove "magnet:", prehaps usless now? results = append(results, r)
results = append(results, r) }
} }
}
if len(results) == 0 {
if len(results) == 0 { printWarn("No file results found for query: %s, trying other nodes", query)
log.Printf("No file results found for query: %s, trying other nodes", query) results = tryOtherNodesForFileSearch(query, safe, lang, page, []string{hostID})
results = tryOtherNodesForFileSearch(query, safe, lang, page, []string{hostID}) }
}
return results
return results }
}
func removeMagnetLink(magnet string) string {
func removeMagnetLink(magnet string) string { // Remove the magnet: prefix unconditionally
// Remove the magnet: prefix unconditionally return strings.TrimPrefix(magnet, "magnet:")
return strings.TrimPrefix(magnet, "magnet:") }
}
func parseInt(s string) int {
func parseInt(s string) int { i, err := strconv.Atoi(s)
i, err := strconv.Atoi(s) if err != nil {
if err != nil { return 0
return 0 }
} return i
return i }
}
func parseSize(sizeStr string) int64 {
func parseSize(sizeStr string) int64 { sizeStr = strings.TrimSpace(sizeStr)
sizeStr = strings.TrimSpace(sizeStr) if sizeStr == "" {
if sizeStr == "" { return 0
return 0 }
}
// Use regex to extract numeric value and unit separately
// Use regex to extract numeric value and unit separately re := regexp.MustCompile(`(?i)([\d.]+)\s*([KMGT]?B)`)
re := regexp.MustCompile(`(?i)([\d.]+)\s*([KMGT]?B)`) matches := re.FindStringSubmatch(sizeStr)
matches := re.FindStringSubmatch(sizeStr) if len(matches) < 3 {
if len(matches) < 3 { printWarn("Error parsing size: invalid format %s", sizeStr)
log.Printf("Error parsing size: invalid format %s", sizeStr) return 0
return 0 }
}
sizeStr = matches[1]
sizeStr = matches[1] unit := strings.ToUpper(matches[2])
unit := strings.ToUpper(matches[2])
var multiplier int64 = 1
var multiplier int64 = 1 switch unit {
switch unit { case "KB":
case "KB": multiplier = 1024
multiplier = 1024 case "MB":
case "MB": multiplier = 1024 * 1024
multiplier = 1024 * 1024 case "GB":
case "GB": multiplier = 1024 * 1024 * 1024
multiplier = 1024 * 1024 * 1024 case "TB":
case "TB": multiplier = 1024 * 1024 * 1024 * 1024
multiplier = 1024 * 1024 * 1024 * 1024 default:
default: printWarn("Unknown unit: %s", unit)
log.Printf("Unknown unit: %s", unit) return 0
return 0 }
}
size, err := strconv.ParseFloat(sizeStr, 64)
size, err := strconv.ParseFloat(sizeStr, 64) if err != nil {
if err != nil { printWarn("Error parsing size: %v", err)
log.Printf("Error parsing size: %v", err) return 0
return 0 }
} return int64(size * float64(multiplier))
return int64(size * float64(multiplier)) }
}
// apparently this is needed so it can announce that magnet link is being used and people start seeding it, but I dont like the fact that I add trackers purposefully
// apparently this is needed so it can announce that magnet link is being used and people start seeding it, but I dont like the fact that I add trackers purposefully func applyTrackers(magnetLink string) string {
func applyTrackers(magnetLink string) string { if magnetLink == "" {
if magnetLink == "" { return ""
return "" }
} trackers := []string{
trackers := []string{ "udp://tracker.openbittorrent.com:80/announce",
"udp://tracker.openbittorrent.com:80/announce", "udp://tracker.opentrackr.org:1337/announce",
"udp://tracker.opentrackr.org:1337/announce", "udp://tracker.coppersurfer.tk:6969/announce",
"udp://tracker.coppersurfer.tk:6969/announce", "udp://tracker.leechers-paradise.org:6969/announce",
"udp://tracker.leechers-paradise.org:6969/announce", }
} for _, tracker := range trackers {
for _, tracker := range trackers { magnetLink += "&tr=" + url.QueryEscape(tracker)
magnetLink += "&tr=" + url.QueryEscape(tracker) }
} return magnetLink
return magnetLink }
}
func formatSize(size int64) string {
func formatSize(size int64) string { if size >= 1024*1024*1024*1024 {
if size >= 1024*1024*1024*1024 { return fmt.Sprintf("%.2f TB", float64(size)/(1024*1024*1024*1024))
return fmt.Sprintf("%.2f TB", float64(size)/(1024*1024*1024*1024)) } else if size >= 1024*1024*1024 {
} else if size >= 1024*1024*1024 { return fmt.Sprintf("%.2f GB", float64(size)/(1024*1024*1024))
return fmt.Sprintf("%.2f GB", float64(size)/(1024*1024*1024)) } else if size >= 1024*1024 {
} else if size >= 1024*1024 { return fmt.Sprintf("%.2f MB", float64(size)/(1024*1024))
return fmt.Sprintf("%.2f MB", float64(size)/(1024*1024)) } else if size >= 1024 {
} else if size >= 1024 { return fmt.Sprintf("%.2f KB", float64(size)/1024)
return fmt.Sprintf("%.2f KB", float64(size)/1024) }
} return fmt.Sprintf("%d B", size)
return fmt.Sprintf("%d B", size) }
}
func sanitizeFileName(name string) string {
func sanitizeFileName(name string) string { // Replace spaces with dashes
// Replace spaces with dashes sanitized := regexp.MustCompile(`\s+`).ReplaceAllString(name, "-")
sanitized := regexp.MustCompile(`\s+`).ReplaceAllString(name, "-") // Remove any characters that are not alphanumeric, dashes, or parentheses
// Remove any characters that are not alphanumeric, dashes, or parentheses sanitized = regexp.MustCompile(`[^a-zA-Z0-9\-\(\)]`).ReplaceAllString(sanitized, "")
sanitized = regexp.MustCompile(`[^a-zA-Z0-9\-\(\)]`).ReplaceAllString(sanitized, "") return sanitized
return sanitized }
}
func contains(slice []string, item string) bool {
func contains(slice []string, item string) bool { for _, v := range slice {
for _, v := range slice { if v == item {
if v == item { return true
return true }
} }
} return false
return false }
}

284
forums.go Normal file → Executable file
View file

@ -1,142 +1,142 @@
package main package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"html/template" "html/template"
"log" "log"
"math" "math"
"net/http" "net/http"
"net/url" "net/url"
"time" "time"
) )
func PerformRedditSearch(query string, safe string, page int) ([]ForumSearchResult, error) { func PerformRedditSearch(query string, safe string, page int) ([]ForumSearchResult, error) {
const ( const (
pageSize = 25 pageSize = 25
baseURL = "https://www.reddit.com" baseURL = "https://www.reddit.com"
maxRetries = 5 maxRetries = 5
initialBackoff = 2 * time.Second initialBackoff = 2 * time.Second
) )
var results []ForumSearchResult var results []ForumSearchResult
searchURL := fmt.Sprintf("%s/search.json?q=%s&limit=%d&start=%d", baseURL, url.QueryEscape(query), pageSize, page*pageSize) searchURL := fmt.Sprintf("%s/search.json?q=%s&limit=%d&start=%d", baseURL, url.QueryEscape(query), pageSize, page*pageSize)
var resp *http.Response var resp *http.Response
var err error var err error
// Retry logic with exponential backoff // Retry logic with exponential backoff
for i := 0; i <= maxRetries; i++ { for i := 0; i <= maxRetries; i++ {
resp, err = http.Get(searchURL) resp, err = http.Get(searchURL)
if err != nil { if err != nil {
return nil, fmt.Errorf("making request: %v", err) return nil, fmt.Errorf("making request: %v", err)
} }
if resp.StatusCode != http.StatusTooManyRequests { if resp.StatusCode != http.StatusTooManyRequests {
break break
} }
// Wait for some time before retrying // Wait for some time before retrying
backoff := time.Duration(math.Pow(2, float64(i))) * initialBackoff backoff := time.Duration(math.Pow(2, float64(i))) * initialBackoff
time.Sleep(backoff) time.Sleep(backoff)
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("making request: %v", err) return nil, fmt.Errorf("making request: %v", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
} }
var searchResults map[string]interface{} var searchResults map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&searchResults); err != nil { if err := json.NewDecoder(resp.Body).Decode(&searchResults); err != nil {
return nil, fmt.Errorf("decoding response: %v", err) return nil, fmt.Errorf("decoding response: %v", err)
} }
data, ok := searchResults["data"].(map[string]interface{}) data, ok := searchResults["data"].(map[string]interface{})
if !ok { if !ok {
return nil, fmt.Errorf("no data field in response") return nil, fmt.Errorf("no data field in response")
} }
posts, ok := data["children"].([]interface{}) posts, ok := data["children"].([]interface{})
if !ok { if !ok {
return nil, fmt.Errorf("no children field in data") return nil, fmt.Errorf("no children field in data")
} }
for _, post := range posts { for _, post := range posts {
postData := post.(map[string]interface{})["data"].(map[string]interface{}) postData := post.(map[string]interface{})["data"].(map[string]interface{})
if safe == "active" && postData["over_18"].(bool) { if safe == "active" && postData["over_18"].(bool) {
continue continue
} }
header := postData["title"].(string) header := postData["title"].(string)
description := postData["selftext"].(string) description := postData["selftext"].(string)
if len(description) > 500 { if len(description) > 500 {
description = description[:500] + "..." description = description[:500] + "..."
} }
publishedDate := time.Unix(int64(postData["created_utc"].(float64)), 0) publishedDate := time.Unix(int64(postData["created_utc"].(float64)), 0)
permalink := postData["permalink"].(string) permalink := postData["permalink"].(string)
resultURL := fmt.Sprintf("%s%s", baseURL, permalink) resultURL := fmt.Sprintf("%s%s", baseURL, permalink)
result := ForumSearchResult{ result := ForumSearchResult{
URL: resultURL, URL: resultURL,
Header: header, Header: header,
Description: description, Description: description,
PublishedDate: publishedDate, PublishedDate: publishedDate,
} }
thumbnail := postData["thumbnail"].(string) thumbnail := postData["thumbnail"].(string)
if parsedURL, err := url.Parse(thumbnail); err == nil && parsedURL.Scheme != "" { if parsedURL, err := url.Parse(thumbnail); err == nil && parsedURL.Scheme != "" {
result.ImgSrc = postData["url"].(string) result.ImgSrc = postData["url"].(string)
result.ThumbnailSrc = thumbnail result.ThumbnailSrc = thumbnail
} }
results = append(results, result) results = append(results, result)
} }
return results, nil return results, nil
} }
func handleForumsSearch(w http.ResponseWriter, settings UserSettings, query, safe, lang string, page int) { func handleForumsSearch(w http.ResponseWriter, settings UserSettings, query string, page int) {
results, err := PerformRedditSearch(query, safe, page) results, err := PerformRedditSearch(query, settings.SafeSearch, page)
if err != nil || len(results) == 0 { // 0 == 0 to force search by other node if err != nil || len(results) == 0 { // 0 == 0 to force search by other node
log.Printf("No results from primary search, trying other nodes") log.Printf("No results from primary search, trying other nodes")
results = tryOtherNodesForForumSearch(query, safe, lang, page) results = tryOtherNodesForForumSearch(query, settings.SafeSearch, settings.Language, page)
} }
data := struct { data := struct {
Query string Query string
Results []ForumSearchResult Results []ForumSearchResult
LanguageOptions []LanguageOption LanguageOptions []LanguageOption
CurrentLang string CurrentLang string
Page int Page int
HasPrevPage bool HasPrevPage bool
HasNextPage bool HasNextPage bool
Theme string Theme string
}{ }{
Query: query, Query: query,
Results: results, Results: results,
LanguageOptions: languageOptions, LanguageOptions: languageOptions,
CurrentLang: lang, CurrentLang: settings.Language,
Page: page, Page: page,
HasPrevPage: page > 1, HasPrevPage: page > 1,
HasNextPage: len(results) == 25, HasNextPage: len(results) == 25,
Theme: settings.Theme, Theme: settings.Theme,
} }
funcMap := template.FuncMap{ funcMap := template.FuncMap{
"sub": func(a, b int) int { return a - b }, "sub": func(a, b int) int { return a - b },
"add": func(a, b int) int { return a + b }, "add": func(a, b int) int { return a + b },
} }
tmpl, err := template.New("forums.html").Funcs(funcMap).ParseFiles("templates/forums.html") tmpl, err := template.New("forums.html").Funcs(funcMap).ParseFiles("templates/forums.html")
if err != nil { if err != nil {
http.Error(w, fmt.Sprintf("Error loading template: %v", err), http.StatusInternalServerError) http.Error(w, fmt.Sprintf("Error loading template: %v", err), http.StatusInternalServerError)
return return
} }
if err := tmpl.Execute(w, data); err != nil { if err := tmpl.Execute(w, data); err != nil {
http.Error(w, fmt.Sprintf("Error rendering template: %v", err), http.StatusInternalServerError) http.Error(w, fmt.Sprintf("Error rendering template: %v", err), http.StatusInternalServerError)
} }
} }

294
images.go Normal file → Executable file
View file

@ -1,147 +1,147 @@
package main package main
import ( import (
"fmt" "fmt"
"html/template" "html/template"
"log" "log"
"net/http" "net/http"
"time" "time"
) )
var imageSearchEngines []SearchEngine var imageSearchEngines []SearchEngine
func init() { func init() {
imageSearchEngines = []SearchEngine{ imageSearchEngines = []SearchEngine{
{Name: "Qwant", Func: wrapImageSearchFunc(PerformQwantImageSearch), Weight: 1}, {Name: "Qwant", Func: wrapImageSearchFunc(PerformQwantImageSearch), Weight: 1},
{Name: "Bing", Func: wrapImageSearchFunc(PerformBingImageSearch), Weight: 2}, // Bing sometimes returns with low amount of images, this leads to danamica page loading 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}, {Name: "Imgur", Func: wrapImageSearchFunc(PerformImgurImageSearch), Weight: 3},
} }
} }
func handleImageSearch(w http.ResponseWriter, settings UserSettings, query, safe, lang string, page int) { func handleImageSearch(w http.ResponseWriter, settings UserSettings, query string, page int) {
startTime := time.Now() startTime := time.Now()
cacheKey := CacheKey{Query: query, Page: page, Safe: safe == "true", Lang: lang, Type: "image"} cacheKey := CacheKey{Query: query, Page: page, Safe: settings.SafeSearch == "true", Lang: settings.Language, Type: "image"}
combinedResults := getImageResultsFromCacheOrFetch(cacheKey, query, safe, lang, page) combinedResults := getImageResultsFromCacheOrFetch(cacheKey, query, settings.SafeSearch, settings.Language, page)
elapsedTime := time.Since(startTime) elapsedTime := time.Since(startTime)
tmpl, err := template.New("images.html").Funcs(funcs).ParseFiles("templates/images.html") tmpl, err := template.New("images.html").Funcs(funcs).ParseFiles("templates/images.html")
if err != nil { if err != nil {
log.Printf("Error parsing template: %v", err) log.Printf("Error parsing template: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError) http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return return
} }
data := struct { data := struct {
Results []ImageSearchResult Results []ImageSearchResult
Query string Query string
Page int Page int
Fetched string Fetched string
LanguageOptions []LanguageOption LanguageOptions []LanguageOption
CurrentLang string CurrentLang string
HasPrevPage bool HasPrevPage bool
HasNextPage bool HasNextPage bool
NoResults bool NoResults bool
Theme string Theme string
}{ }{
Results: combinedResults, Results: combinedResults,
Query: query, Query: query,
Page: page, Page: page,
Fetched: fmt.Sprintf("%.2f seconds", elapsedTime.Seconds()), Fetched: fmt.Sprintf("%.2f seconds", elapsedTime.Seconds()),
LanguageOptions: languageOptions, LanguageOptions: languageOptions,
CurrentLang: lang, CurrentLang: settings.Language,
HasPrevPage: page > 1, HasPrevPage: page > 1,
HasNextPage: len(combinedResults) >= 50, HasNextPage: len(combinedResults) >= 50,
NoResults: len(combinedResults) == 0, NoResults: len(combinedResults) == 0,
Theme: settings.Theme, Theme: settings.Theme,
} }
err = tmpl.Execute(w, data) err = tmpl.Execute(w, data)
if err != nil { if err != nil {
log.Printf("Error executing template: %v", err) printErr("Error executing template: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError) http.Error(w, "Internal Server Error", http.StatusInternalServerError)
} }
} }
func getImageResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string, page int) []ImageSearchResult { func getImageResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string, page int) []ImageSearchResult {
cacheChan := make(chan []SearchResult) cacheChan := make(chan []SearchResult)
var combinedResults []ImageSearchResult var combinedResults []ImageSearchResult
go func() { go func() {
results, exists := resultsCache.Get(cacheKey) results, exists := resultsCache.Get(cacheKey)
if exists { if exists {
log.Println("Cache hit") printInfo("Cache hit")
cacheChan <- results cacheChan <- results
} else { } else {
log.Println("Cache miss") printInfo("Cache miss")
cacheChan <- nil cacheChan <- nil
} }
}() }()
select { select {
case results := <-cacheChan: case results := <-cacheChan:
if results == nil { if results == nil {
combinedResults = fetchImageResults(query, safe, lang, page) combinedResults = fetchImageResults(query, safe, lang, page)
if len(combinedResults) > 0 { if len(combinedResults) > 0 {
resultsCache.Set(cacheKey, convertToSearchResults(combinedResults)) resultsCache.Set(cacheKey, convertToSearchResults(combinedResults))
} }
} else { } else {
_, _, imageResults := convertToSpecificResults(results) _, _, imageResults := convertToSpecificResults(results)
combinedResults = imageResults combinedResults = imageResults
} }
case <-time.After(2 * time.Second): case <-time.After(2 * time.Second):
log.Println("Cache check timeout") printInfo("Cache check timeout")
combinedResults = fetchImageResults(query, safe, lang, page) combinedResults = fetchImageResults(query, safe, lang, page)
if len(combinedResults) > 0 { if len(combinedResults) > 0 {
resultsCache.Set(cacheKey, convertToSearchResults(combinedResults)) resultsCache.Set(cacheKey, convertToSearchResults(combinedResults))
} }
} }
return combinedResults return combinedResults
} }
func fetchImageResults(query, safe, lang string, page int) []ImageSearchResult { func fetchImageResults(query, safe, lang string, page int) []ImageSearchResult {
var results []ImageSearchResult var results []ImageSearchResult
for _, engine := range imageSearchEngines { for _, engine := range imageSearchEngines {
log.Printf("Using image search engine: %s", engine.Name) printInfo("Using image search engine: %s", engine.Name)
searchResults, duration, err := engine.Func(query, safe, lang, page) searchResults, duration, err := engine.Func(query, safe, lang, page)
updateEngineMetrics(&engine, duration, err == nil) updateEngineMetrics(&engine, duration, err == nil)
if err != nil { if err != nil {
log.Printf("Error performing image search with %s: %v", engine.Name, err) printWarn("Error performing image search with %s: %v", engine.Name, err)
continue continue
} }
for _, result := range searchResults { for _, result := range searchResults {
results = append(results, result.(ImageSearchResult)) results = append(results, result.(ImageSearchResult))
} }
// If results are found, break out of the loop // If results are found, break out of the loop
if len(results) > 0 { if len(results) > 0 {
break break
} }
} }
// If no results found after trying all engines // If no results found after trying all engines
if len(results) == 0 { if len(results) == 0 {
log.Printf("No image results found for query: %s, trying other nodes", query) printWarn("No image results found for query: %s, trying other nodes", query)
results = tryOtherNodesForImageSearch(query, safe, lang, page, []string{hostID}) results = tryOtherNodesForImageSearch(query, safe, lang, page, []string{hostID})
} }
return results return results
} }
func wrapImageSearchFunc(f func(string, string, string, int) ([]ImageSearchResult, time.Duration, error)) func(string, string, string, int) ([]SearchResult, time.Duration, error) { func wrapImageSearchFunc(f func(string, string, string, int) ([]ImageSearchResult, time.Duration, error)) func(string, string, string, int) ([]SearchResult, time.Duration, error) {
return func(query, safe, lang string, page int) ([]SearchResult, time.Duration, error) { return func(query, safe, lang string, page int) ([]SearchResult, time.Duration, error) {
imageResults, duration, err := f(query, safe, lang, page) imageResults, duration, err := f(query, safe, lang, page)
if err != nil { if err != nil {
return nil, duration, err return nil, duration, err
} }
searchResults := make([]SearchResult, len(imageResults)) searchResults := make([]SearchResult, len(imageResults))
for i, result := range imageResults { for i, result := range imageResults {
searchResults[i] = result searchResults[i] = result
} }
return searchResults, duration, nil return searchResults, duration, nil
} }
} }

355
main.go Normal file → Executable file
View file

@ -1,178 +1,177 @@
package main package main
import ( import (
"fmt" "fmt"
"html/template" "html/template"
"log" "log"
"net/http" "net/http"
"strconv" "strconv"
) )
// LanguageOption represents a language option for search // LanguageOption represents a language option for search
type LanguageOption struct { type LanguageOption struct {
Code string Code string
Name string Name string
} }
var settings UserSettings var settings UserSettings
var languageOptions = []LanguageOption{ var languageOptions = []LanguageOption{
{Code: "", Name: "Any Language"}, {Code: "", Name: "Any Language"},
{Code: "lang_en", Name: "English"}, {Code: "lang_en", Name: "English"},
{Code: "lang_af", Name: "Afrikaans"}, {Code: "lang_af", Name: "Afrikaans"},
{Code: "lang_ar", Name: "العربية (Arabic)"}, {Code: "lang_ar", Name: "العربية (Arabic)"},
{Code: "lang_hy", Name: "Հայերեն (Armenian)"}, {Code: "lang_hy", Name: "Հայերեն (Armenian)"},
{Code: "lang_be", Name: "Беларуская (Belarusian)"}, {Code: "lang_be", Name: "Беларуская (Belarusian)"},
{Code: "lang_bg", Name: "български (Bulgarian)"}, {Code: "lang_bg", Name: "български (Bulgarian)"},
{Code: "lang_ca", Name: "Català (Catalan)"}, {Code: "lang_ca", Name: "Català (Catalan)"},
{Code: "lang_zh-CN", Name: "中文 (简体) (Chinese Simplified)"}, {Code: "lang_zh-CN", Name: "中文 (简体) (Chinese Simplified)"},
{Code: "lang_zh-TW", Name: "中文 (繁體) (Chinese Traditional)"}, {Code: "lang_zh-TW", Name: "中文 (繁體) (Chinese Traditional)"},
{Code: "lang_hr", Name: "Hrvatski (Croatian)"}, {Code: "lang_hr", Name: "Hrvatski (Croatian)"},
{Code: "lang_cs", Name: "Čeština (Czech)"}, {Code: "lang_cs", Name: "Čeština (Czech)"},
{Code: "lang_da", Name: "Dansk (Danish)"}, {Code: "lang_da", Name: "Dansk (Danish)"},
{Code: "lang_nl", Name: "Nederlands (Dutch)"}, {Code: "lang_nl", Name: "Nederlands (Dutch)"},
{Code: "lang_eo", Name: "Esperanto"}, {Code: "lang_eo", Name: "Esperanto"},
{Code: "lang_et", Name: "Eesti (Estonian)"}, {Code: "lang_et", Name: "Eesti (Estonian)"},
{Code: "lang_tl", Name: "Filipino (Tagalog)"}, {Code: "lang_tl", Name: "Filipino (Tagalog)"},
{Code: "lang_fi", Name: "Suomi (Finnish)"}, {Code: "lang_fi", Name: "Suomi (Finnish)"},
{Code: "lang_fr", Name: "Français (French)"}, {Code: "lang_fr", Name: "Français (French)"},
{Code: "lang_de", Name: "Deutsch (German)"}, {Code: "lang_de", Name: "Deutsch (German)"},
{Code: "lang_el", Name: "Ελληνικά (Greek)"}, {Code: "lang_el", Name: "Ελληνικά (Greek)"},
{Code: "lang_iw", Name: "עברית (Hebrew)"}, {Code: "lang_iw", Name: "עברית (Hebrew)"},
{Code: "lang_hi", Name: "हिन्दी (Hindi)"}, {Code: "lang_hi", Name: "हिन्दी (Hindi)"},
{Code: "lang_hu", Name: "magyar (Hungarian)"}, {Code: "lang_hu", Name: "magyar (Hungarian)"},
{Code: "lang_is", Name: "íslenska (Icelandic)"}, {Code: "lang_is", Name: "íslenska (Icelandic)"},
{Code: "lang_id", Name: "Bahasa Indonesia (Indonesian)"}, {Code: "lang_id", Name: "Bahasa Indonesia (Indonesian)"},
{Code: "lang_it", Name: "italiano (Italian)"}, {Code: "lang_it", Name: "italiano (Italian)"},
{Code: "lang_ja", Name: "日本語 (Japanese)"}, {Code: "lang_ja", Name: "日本語 (Japanese)"},
{Code: "lang_ko", Name: "한국어 (Korean)"}, {Code: "lang_ko", Name: "한국어 (Korean)"},
{Code: "lang_lv", Name: "latviešu (Latvian)"}, {Code: "lang_lv", Name: "latviešu (Latvian)"},
{Code: "lang_lt", Name: "lietuvių (Lithuanian)"}, {Code: "lang_lt", Name: "lietuvių (Lithuanian)"},
{Code: "lang_no", Name: "norsk (Norwegian)"}, {Code: "lang_no", Name: "norsk (Norwegian)"},
{Code: "lang_fa", Name: "فارسی (Persian)"}, {Code: "lang_fa", Name: "فارسی (Persian)"},
{Code: "lang_pl", Name: "polski (Polish)"}, {Code: "lang_pl", Name: "polski (Polish)"},
{Code: "lang_pt", Name: "português (Portuguese)"}, {Code: "lang_pt", Name: "português (Portuguese)"},
{Code: "lang_ro", Name: "română (Romanian)"}, {Code: "lang_ro", Name: "română (Romanian)"},
{Code: "lang_ru", Name: "русский (Russian)"}, {Code: "lang_ru", Name: "русский (Russian)"},
{Code: "lang_sr", Name: "српски (Serbian)"}, {Code: "lang_sr", Name: "српски (Serbian)"},
{Code: "lang_sk", Name: "slovenčina (Slovak)"}, {Code: "lang_sk", Name: "slovenčina (Slovak)"},
{Code: "lang_sl", Name: "slovenščina (Slovenian)"}, {Code: "lang_sl", Name: "slovenščina (Slovenian)"},
{Code: "lang_es", Name: "español (Spanish)"}, {Code: "lang_es", Name: "español (Spanish)"},
{Code: "lang_sw", Name: "Kiswahili (Swahili)"}, {Code: "lang_sw", Name: "Kiswahili (Swahili)"},
{Code: "lang_sv", Name: "svenska (Swedish)"}, {Code: "lang_sv", Name: "svenska (Swedish)"},
{Code: "lang_th", Name: "ไทย (Thai)"}, {Code: "lang_th", Name: "ไทย (Thai)"},
{Code: "lang_tr", Name: "Türkçe (Turkish)"}, {Code: "lang_tr", Name: "Türkçe (Turkish)"},
{Code: "lang_uk", Name: "українська (Ukrainian)"}, {Code: "lang_uk", Name: "українська (Ukrainian)"},
{Code: "lang_vi", Name: "Tiếng Việt (Vietnamese)"}, {Code: "lang_vi", Name: "Tiếng Việt (Vietnamese)"},
} }
func handleSearch(w http.ResponseWriter, r *http.Request) { func handleSearch(w http.ResponseWriter, r *http.Request) {
query, safe, lang, searchType, page := parseSearchParams(r) query, safe, lang, searchType, page := parseSearchParams(r)
// Load user settings // Load user settings
settings = loadUserSettings(r) settings = loadUserSettings(r)
// Update the theme, safe search, and language based on query parameters or use existing settings // Update the theme, safe search, and language based on query parameters or use existing settings
theme := r.URL.Query().Get("theme") theme := r.URL.Query().Get("theme")
if theme != "" { if theme != "" {
settings.Theme = theme settings.Theme = theme
saveUserSettings(w, settings) saveUserSettings(w, settings)
} else if settings.Theme == "" { } else if settings.Theme == "" {
settings.Theme = "dark" // Default theme settings.Theme = "dark" // Default theme
} }
if safe != "" { if safe != "" {
settings.SafeSearch = safe settings.SafeSearch = safe
saveUserSettings(w, settings) saveUserSettings(w, settings)
} }
if lang != "" { if lang != "" {
settings.Language = lang settings.Language = lang
saveUserSettings(w, settings) saveUserSettings(w, settings)
} }
// Render the search page template if no query // Render the search page template if no query
if query == "" { if query == "" {
tmpl := template.Must(template.ParseFiles("templates/search.html")) tmpl := template.Must(template.ParseFiles("templates/search.html"))
tmpl.Execute(w, settings) tmpl.Execute(w, settings)
return return
} }
settings := loadUserSettings(r) settings := loadUserSettings(r)
// Handle search based on the type // Handle search based on the type
switch searchType { switch searchType {
case "image": case "image":
handleImageSearch(w, settings, query, safe, lang, page) handleImageSearch(w, settings, query, page)
case "video": case "video":
handleVideoSearch(w, settings, query, safe, lang, page) handleVideoSearch(w, settings, query, page)
case "map": case "map":
handleMapSearch(w, settings, query, safe) handleMapSearch(w, settings, query)
case "forum": case "forum":
handleForumsSearch(w, settings, query, safe, lang, page) handleForumsSearch(w, settings, query, page)
case "file": case "file":
handleFileSearch(w, settings, query, safe, lang, page) handleFileSearch(w, settings, query, page)
case "text": case "text":
fallthrough fallthrough
default: default:
HandleTextSearch(w, settings, query, safe, lang, page) HandleTextSearch(w, settings, query, page)
} }
// This is immeasurably stupid it passes safe and language then it passes settings with safe and lang again }
}
func parseSearchParams(r *http.Request) (query, safe, lang, searchType string, page int) {
func parseSearchParams(r *http.Request) (query, safe, lang, searchType string, page int) { if r.Method == "GET" {
if r.Method == "GET" { query = r.URL.Query().Get("q")
query = r.URL.Query().Get("q") safe = r.URL.Query().Get("safe")
safe = r.URL.Query().Get("safe") lang = r.URL.Query().Get("lang")
lang = r.URL.Query().Get("lang") searchType = r.URL.Query().Get("t")
searchType = r.URL.Query().Get("t") pageStr := r.URL.Query().Get("p")
pageStr := r.URL.Query().Get("p") page = parsePageParameter(pageStr)
page = parsePageParameter(pageStr) } else if r.Method == "POST" {
} else if r.Method == "POST" { query = r.FormValue("q")
query = r.FormValue("q") safe = r.FormValue("safe")
safe = r.FormValue("safe") lang = r.FormValue("lang")
lang = r.FormValue("lang") searchType = r.FormValue("t")
searchType = r.FormValue("t") pageStr := r.FormValue("p")
pageStr := r.FormValue("p") page = parsePageParameter(pageStr)
page = parsePageParameter(pageStr) }
}
if searchType == "" {
if searchType == "" { searchType = "text"
searchType = "text" }
}
return
return }
}
func parsePageParameter(pageStr string) int {
func parsePageParameter(pageStr string) int { page, err := strconv.Atoi(pageStr)
page, err := strconv.Atoi(pageStr) if err != nil || page < 1 {
if err != nil || page < 1 { page = 1
page = 1 }
} return page
return page }
}
func runServer() {
func runServer() { http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) http.HandleFunc("/", handleSearch)
http.HandleFunc("/", handleSearch) http.HandleFunc("/search", handleSearch)
http.HandleFunc("/search", handleSearch) http.HandleFunc("/img_proxy", handleImageProxy)
http.HandleFunc("/img_proxy", handleImageProxy) http.HandleFunc("/node", handleNodeRequest)
http.HandleFunc("/node", handleNodeRequest) http.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) {
http.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "templates/settings.html")
http.ServeFile(w, r, "templates/settings.html") })
}) http.HandleFunc("/opensearch.xml", func(w http.ResponseWriter, r *http.Request) {
http.HandleFunc("/opensearch.xml", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/opensearchdescription+xml")
w.Header().Set("Content-Type", "application/opensearchdescription+xml") http.ServeFile(w, r, "static/opensearch.xml")
http.ServeFile(w, r, "static/opensearch.xml") })
}) initializeTorrentSites()
initializeTorrentSites()
config := loadConfig()
config := loadConfig() generateOpenSearchXML(config)
generateOpenSearchXML(config)
printMessage("Server is listening on http://localhost:%d", config.Port)
printMessage("Server is listening on http://localhost:%d", config.Port) log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", config.Port), nil))
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", config.Port), nil))
// Start automatic update checker
// Start automatic update checker go checkForUpdates()
go checkForUpdates() }
}

144
map.go Normal file → Executable file
View file

@ -1,72 +1,72 @@
package main package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"html/template" "html/template"
"net/http" "net/http"
"net/url" "net/url"
) )
type NominatimResponse struct { type NominatimResponse struct {
Lat string `json:"lat"` Lat string `json:"lat"`
Lon string `json:"lon"` Lon string `json:"lon"`
} }
func geocodeQuery(query string) (latitude, longitude string, found bool, err error) { func geocodeQuery(query string) (latitude, longitude string, found bool, err error) {
// URL encode the query // URL encode the query
query = url.QueryEscape(query) query = url.QueryEscape(query)
// Construct the request URL // Construct the request URL
urlString := fmt.Sprintf("https://nominatim.openstreetmap.org/search?format=json&q=%s", query) urlString := fmt.Sprintf("https://nominatim.openstreetmap.org/search?format=json&q=%s", query)
// Make the HTTP GET request // Make the HTTP GET request
resp, err := http.Get(urlString) resp, err := http.Get(urlString)
if err != nil { if err != nil {
return "", "", false, err return "", "", false, err
} }
defer resp.Body.Close() defer resp.Body.Close()
// Read the response // Read the response
var result []NominatimResponse var result []NominatimResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", "", false, err return "", "", false, err
} }
// Check if there are any results // Check if there are any results
if len(result) > 0 { if len(result) > 0 {
latitude = result[0].Lat latitude = result[0].Lat
longitude = result[0].Lon longitude = result[0].Lon
return latitude, longitude, true, nil return latitude, longitude, true, nil
} }
return "", "", false, nil return "", "", false, nil
} }
func handleMapSearch(w http.ResponseWriter, settings UserSettings, query string, lang string) { func handleMapSearch(w http.ResponseWriter, settings UserSettings, query string) {
// Geocode the query to get coordinates // Geocode the query to get coordinates
latitude, longitude, found, err := geocodeQuery(query) latitude, longitude, found, err := geocodeQuery(query)
if err != nil { if err != nil {
printDebug("Error geocoding query: %s, error: %v", query, err) printDebug("Error geocoding query: %s, error: %v", query, err)
http.Error(w, "Failed to find location", http.StatusInternalServerError) http.Error(w, "Failed to find location", http.StatusInternalServerError)
return return
} }
// Use a template to serve the map page // Use a template to serve the map page
data := map[string]interface{}{ data := map[string]interface{}{
"Query": query, "Query": query,
"Latitude": latitude, "Latitude": latitude,
"Longitude": longitude, "Longitude": longitude,
"Found": found, "Found": found,
"Theme": settings.Theme, "Theme": settings.Theme,
} }
tmpl, err := template.ParseFiles("templates/map.html") tmpl, err := template.ParseFiles("templates/map.html")
if err != nil { if err != nil {
printErr("Error loading map template: %v", err) printErr("Error loading map template: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError) http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return return
} }
tmpl.Execute(w, data) tmpl.Execute(w, data)
} }

438
node-handle-search.go Normal file → Executable file
View file

@ -1,219 +1,219 @@
package main package main
import ( import (
"encoding/json" "encoding/json"
"log" "log"
) )
func handleSearchTextMessage(msg Message) { func handleSearchTextMessage(msg Message) {
var searchParams struct { var searchParams struct {
Query string `json:"query"` Query string `json:"query"`
Safe string `json:"safe"` Safe string `json:"safe"`
Lang string `json:"lang"` Lang string `json:"lang"`
Page int `json:"page"` Page int `json:"page"`
ResponseAddr string `json:"responseAddr"` ResponseAddr string `json:"responseAddr"`
} }
err := json.Unmarshal([]byte(msg.Content), &searchParams) err := json.Unmarshal([]byte(msg.Content), &searchParams)
if err != nil { if err != nil {
log.Printf("Error parsing search parameters: %v", err) printWarn("Error parsing search parameters: %v", err)
return return
} }
log.Printf("Received search-text request. ResponseAddr: %s", searchParams.ResponseAddr) printDebug("Received search-text request. ResponseAddr: %s", searchParams.ResponseAddr)
results := fetchTextResults(searchParams.Query, searchParams.Safe, searchParams.Lang, searchParams.Page) results := fetchTextResults(searchParams.Query, searchParams.Safe, searchParams.Lang, searchParams.Page)
resultsJSON, err := json.Marshal(results) resultsJSON, err := json.Marshal(results)
if err != nil { if err != nil {
log.Printf("Error marshalling search results: %v", err) printWarn("Error marshalling search results: %v", err)
return return
} }
responseMsg := Message{ responseMsg := Message{
ID: hostID, ID: hostID,
Type: "text-results", Type: "text-results",
Content: string(resultsJSON), Content: string(resultsJSON),
} }
// Log the address to be used for sending the response // Log the address to be used for sending the response
log.Printf("Sending text search results to %s", searchParams.ResponseAddr) printDebug("Sending text search results to %s", searchParams.ResponseAddr)
if searchParams.ResponseAddr == "" { if searchParams.ResponseAddr == "" {
log.Printf("Error: Response address is empty") printErr("Error: Response address is empty")
return return
} }
err = sendMessage(searchParams.ResponseAddr, responseMsg) err = sendMessage(searchParams.ResponseAddr, responseMsg)
if err != nil { if err != nil {
log.Printf("Error sending text search results to %s: %v", searchParams.ResponseAddr, err) printWarn("Error sending text search results to %s: %v", searchParams.ResponseAddr, err)
} }
} }
func handleSearchImageMessage(msg Message) { func handleSearchImageMessage(msg Message) {
var searchParams struct { var searchParams struct {
Query string `json:"query"` Query string `json:"query"`
Safe string `json:"safe"` Safe string `json:"safe"`
Lang string `json:"lang"` Lang string `json:"lang"`
Page int `json:"page"` Page int `json:"page"`
ResponseAddr string `json:"responseAddr"` ResponseAddr string `json:"responseAddr"`
} }
err := json.Unmarshal([]byte(msg.Content), &searchParams) err := json.Unmarshal([]byte(msg.Content), &searchParams)
if err != nil { if err != nil {
log.Printf("Error parsing search parameters: %v", err) log.Printf("Error parsing search parameters: %v", err)
return return
} }
log.Printf("Received search-image request. ResponseAddr: %s", searchParams.ResponseAddr) log.Printf("Received search-image request. ResponseAddr: %s", searchParams.ResponseAddr)
results := fetchImageResults(searchParams.Query, searchParams.Safe, searchParams.Lang, searchParams.Page) results := fetchImageResults(searchParams.Query, searchParams.Safe, searchParams.Lang, searchParams.Page)
resultsJSON, err := json.Marshal(results) resultsJSON, err := json.Marshal(results)
if err != nil { if err != nil {
log.Printf("Error marshalling search results: %v", err) log.Printf("Error marshalling search results: %v", err)
return return
} }
responseMsg := Message{ responseMsg := Message{
ID: hostID, ID: hostID,
Type: "image-results", Type: "image-results",
Content: string(resultsJSON), Content: string(resultsJSON),
} }
// Log the address to be used for sending the response // Log the address to be used for sending the response
log.Printf("Sending image search results to %s", searchParams.ResponseAddr) log.Printf("Sending image search results to %s", searchParams.ResponseAddr)
if searchParams.ResponseAddr == "" { if searchParams.ResponseAddr == "" {
log.Printf("Error: Response address is empty") log.Printf("Error: Response address is empty")
return return
} }
err = sendMessage(searchParams.ResponseAddr, responseMsg) err = sendMessage(searchParams.ResponseAddr, responseMsg)
if err != nil { if err != nil {
log.Printf("Error sending image search results to %s: %v", searchParams.ResponseAddr, err) log.Printf("Error sending image search results to %s: %v", searchParams.ResponseAddr, err)
} }
} }
func handleSearchVideoMessage(msg Message) { func handleSearchVideoMessage(msg Message) {
var searchParams struct { var searchParams struct {
Query string `json:"query"` Query string `json:"query"`
Safe string `json:"safe"` Safe string `json:"safe"`
Lang string `json:"lang"` Lang string `json:"lang"`
Page int `json:"page"` Page int `json:"page"`
ResponseAddr string `json:"responseAddr"` ResponseAddr string `json:"responseAddr"`
} }
err := json.Unmarshal([]byte(msg.Content), &searchParams) err := json.Unmarshal([]byte(msg.Content), &searchParams)
if err != nil { if err != nil {
log.Printf("Error parsing search parameters: %v", err) log.Printf("Error parsing search parameters: %v", err)
return return
} }
log.Printf("Received search-video request. ResponseAddr: %s", searchParams.ResponseAddr) log.Printf("Received search-video request. ResponseAddr: %s", searchParams.ResponseAddr)
results := fetchVideoResults(searchParams.Query, searchParams.Safe, searchParams.Lang, searchParams.Page) results := fetchVideoResults(searchParams.Query, searchParams.Safe, searchParams.Lang, searchParams.Page)
resultsJSON, err := json.Marshal(results) resultsJSON, err := json.Marshal(results)
if err != nil { if err != nil {
log.Printf("Error marshalling search results: %v", err) log.Printf("Error marshalling search results: %v", err)
return return
} }
responseMsg := Message{ responseMsg := Message{
ID: hostID, ID: hostID,
Type: "video-results", Type: "video-results",
Content: string(resultsJSON), Content: string(resultsJSON),
} }
log.Printf("Sending video search results to %s", searchParams.ResponseAddr) log.Printf("Sending video search results to %s", searchParams.ResponseAddr)
if searchParams.ResponseAddr == "" { if searchParams.ResponseAddr == "" {
log.Printf("Error: Response address is empty") log.Printf("Error: Response address is empty")
return return
} }
err = sendMessage(searchParams.ResponseAddr, responseMsg) err = sendMessage(searchParams.ResponseAddr, responseMsg)
if err != nil { if err != nil {
log.Printf("Error sending video search results to %s: %v", searchParams.ResponseAddr, err) log.Printf("Error sending video search results to %s: %v", searchParams.ResponseAddr, err)
} }
} }
func handleSearchFileMessage(msg Message) { func handleSearchFileMessage(msg Message) {
var searchParams struct { var searchParams struct {
Query string `json:"query"` Query string `json:"query"`
Safe string `json:"safe"` Safe string `json:"safe"`
Lang string `json:"lang"` Lang string `json:"lang"`
Page int `json:"page"` Page int `json:"page"`
ResponseAddr string `json:"responseAddr"` ResponseAddr string `json:"responseAddr"`
} }
err := json.Unmarshal([]byte(msg.Content), &searchParams) err := json.Unmarshal([]byte(msg.Content), &searchParams)
if err != nil { if err != nil {
log.Printf("Error parsing search parameters: %v", err) log.Printf("Error parsing search parameters: %v", err)
return return
} }
log.Printf("Received search-file request. ResponseAddr: %s", searchParams.ResponseAddr) log.Printf("Received search-file request. ResponseAddr: %s", searchParams.ResponseAddr)
results := fetchFileResults(searchParams.Query, searchParams.Safe, searchParams.Lang, searchParams.Page) results := fetchFileResults(searchParams.Query, searchParams.Safe, searchParams.Lang, searchParams.Page)
resultsJSON, err := json.Marshal(results) resultsJSON, err := json.Marshal(results)
if err != nil { if err != nil {
log.Printf("Error marshalling search results: %v", err) log.Printf("Error marshalling search results: %v", err)
return return
} }
responseMsg := Message{ responseMsg := Message{
ID: hostID, ID: hostID,
Type: "file-results", Type: "file-results",
Content: string(resultsJSON), Content: string(resultsJSON),
} }
log.Printf("Sending file search results to %s", searchParams.ResponseAddr) log.Printf("Sending file search results to %s", searchParams.ResponseAddr)
if searchParams.ResponseAddr == "" { if searchParams.ResponseAddr == "" {
log.Printf("Error: Response address is empty") log.Printf("Error: Response address is empty")
return return
} }
err = sendMessage(searchParams.ResponseAddr, responseMsg) err = sendMessage(searchParams.ResponseAddr, responseMsg)
if err != nil { if err != nil {
log.Printf("Error sending file search results to %s: %v", searchParams.ResponseAddr, err) log.Printf("Error sending file search results to %s: %v", searchParams.ResponseAddr, err)
} }
} }
func handleSearchForumMessage(msg Message) { func handleSearchForumMessage(msg Message) {
var searchParams struct { var searchParams struct {
Query string `json:"query"` Query string `json:"query"`
Safe string `json:"safe"` Safe string `json:"safe"`
Lang string `json:"lang"` Lang string `json:"lang"`
Page int `json:"page"` Page int `json:"page"`
ResponseAddr string `json:"responseAddr"` ResponseAddr string `json:"responseAddr"`
} }
err := json.Unmarshal([]byte(msg.Content), &searchParams) err := json.Unmarshal([]byte(msg.Content), &searchParams)
if err != nil { if err != nil {
log.Printf("Error parsing search parameters: %v", err) log.Printf("Error parsing search parameters: %v", err)
return return
} }
log.Printf("Received search-forum request. ResponseAddr: %s", searchParams.ResponseAddr) log.Printf("Received search-forum request. ResponseAddr: %s", searchParams.ResponseAddr)
results := fetchForumResults(searchParams.Query, searchParams.Safe, searchParams.Lang, searchParams.Page) results := fetchForumResults(searchParams.Query, searchParams.Safe, searchParams.Lang, searchParams.Page)
resultsJSON, err := json.Marshal(results) resultsJSON, err := json.Marshal(results)
if err != nil { if err != nil {
log.Printf("Error marshalling search results: %v", err) log.Printf("Error marshalling search results: %v", err)
return return
} }
responseMsg := Message{ responseMsg := Message{
ID: hostID, ID: hostID,
Type: "forum-results", Type: "forum-results",
Content: string(resultsJSON), Content: string(resultsJSON),
} }
// Log the address to be used for sending the response // Log the address to be used for sending the response
log.Printf("Sending forum search results to %s", searchParams.ResponseAddr) log.Printf("Sending forum search results to %s", searchParams.ResponseAddr)
if searchParams.ResponseAddr == "" { if searchParams.ResponseAddr == "" {
log.Printf("Error: Response address is empty") log.Printf("Error: Response address is empty")
return return
} }
err = sendMessage(searchParams.ResponseAddr, responseMsg) err = sendMessage(searchParams.ResponseAddr, responseMsg)
if err != nil { if err != nil {
log.Printf("Error sending forum search results to %s: %v", searchParams.ResponseAddr, err) log.Printf("Error sending forum search results to %s: %v", searchParams.ResponseAddr, err)
} }
} }

165
node-request-files.go Normal file → Executable file
View file

@ -1,83 +1,82 @@
package main package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "time"
"time" )
)
func tryOtherNodesForFileSearch(query, safe, lang string, page int, visitedNodes []string) []TorrentResult {
func tryOtherNodesForFileSearch(query, safe, lang string, page int, visitedNodes []string) []TorrentResult { for _, nodeAddr := range peers {
for _, nodeAddr := range peers { if contains(visitedNodes, nodeAddr) {
if contains(visitedNodes, nodeAddr) { continue // Skip nodes already visited
continue // Skip nodes already visited }
} results, err := sendFileSearchRequestToNode(nodeAddr, query, safe, lang, page, visitedNodes)
results, err := sendFileSearchRequestToNode(nodeAddr, query, safe, lang, page, visitedNodes) if err != nil {
if err != nil { printWarn("Error contacting node %s: %v", nodeAddr, err)
log.Printf("Error contacting node %s: %v", nodeAddr, err) continue
continue }
} if len(results) > 0 {
if len(results) > 0 { return results
return results }
} }
} return nil
return nil }
}
func sendFileSearchRequestToNode(nodeAddr, query, safe, lang string, page int, visitedNodes []string) ([]TorrentResult, error) {
func sendFileSearchRequestToNode(nodeAddr, query, safe, lang string, page int, visitedNodes []string) ([]TorrentResult, error) { visitedNodes = append(visitedNodes, nodeAddr)
visitedNodes = append(visitedNodes, nodeAddr) searchParams := struct {
searchParams := struct { Query string `json:"query"`
Query string `json:"query"` Safe string `json:"safe"`
Safe string `json:"safe"` Lang string `json:"lang"`
Lang string `json:"lang"` Page int `json:"page"`
Page int `json:"page"` ResponseAddr string `json:"responseAddr"`
ResponseAddr string `json:"responseAddr"` VisitedNodes []string `json:"visitedNodes"`
VisitedNodes []string `json:"visitedNodes"` }{
}{ Query: query,
Query: query, Safe: safe,
Safe: safe, Lang: lang,
Lang: lang, Page: page,
Page: page, ResponseAddr: fmt.Sprintf("http://localhost:%d/node", config.Port),
ResponseAddr: fmt.Sprintf("http://localhost:%d/node", config.Port), VisitedNodes: visitedNodes,
VisitedNodes: visitedNodes, }
}
msgBytes, err := json.Marshal(searchParams)
msgBytes, err := json.Marshal(searchParams) if err != nil {
if err != nil { return nil, fmt.Errorf("failed to marshal search parameters: %v", err)
return nil, fmt.Errorf("failed to marshal search parameters: %v", err) }
}
msg := Message{
msg := Message{ ID: hostID,
ID: hostID, Type: "search-file",
Type: "search-file", Content: string(msgBytes),
Content: string(msgBytes), }
}
err = sendMessage(nodeAddr, msg)
err = sendMessage(nodeAddr, msg) if err != nil {
if err != nil { return nil, fmt.Errorf("failed to send search request to node %s: %v", nodeAddr, err)
return nil, fmt.Errorf("failed to send search request to node %s: %v", nodeAddr, err) }
}
// Wait for results
// Wait for results select {
select { case res := <-fileResultsChan:
case res := <-fileResultsChan: return res, nil
return res, nil case <-time.After(20 * time.Second):
case <-time.After(20 * time.Second): return nil, fmt.Errorf("timeout waiting for results from node %s", nodeAddr)
return nil, fmt.Errorf("timeout waiting for results from node %s", nodeAddr) }
} }
}
func handleFileResultsMessage(msg Message) {
func handleFileResultsMessage(msg Message) { var results []TorrentResult
var results []TorrentResult err := json.Unmarshal([]byte(msg.Content), &results)
err := json.Unmarshal([]byte(msg.Content), &results) if err != nil {
if err != nil { printWarn("Error unmarshalling file results: %v", err)
log.Printf("Error unmarshalling file results: %v", err) return
return }
}
printDebug("Received file results: %+v", results)
log.Printf("Received file results: %+v", results) // Send results to fileResultsChan
// Send results to fileResultsChan go func() {
go func() { fileResultsChan <- results
fileResultsChan <- results }()
}() }
}

201
node-request-forums.go Normal file → Executable file
View file

@ -1,101 +1,100 @@
package main package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "time"
"time" )
)
var forumResultsChan = make(chan []ForumSearchResult)
var forumResultsChan = make(chan []ForumSearchResult)
func tryOtherNodesForForumSearch(query, safe, lang string, page int) []ForumSearchResult {
func tryOtherNodesForForumSearch(query, safe, lang string, page int) []ForumSearchResult { for _, nodeAddr := range peers {
for _, nodeAddr := range peers { results, err := sendForumSearchRequestToNode(nodeAddr, query, safe, lang, page, []string{})
results, err := sendForumSearchRequestToNode(nodeAddr, query, safe, lang, page, []string{}) if err != nil {
if err != nil { printWarn("Error contacting node %s: %v", nodeAddr, err)
log.Printf("Error contacting node %s: %v", nodeAddr, err) continue
continue }
} if len(results) > 0 {
if len(results) > 0 { return results
return results }
} }
} return nil
return nil }
}
func sendForumSearchRequestToNode(nodeAddr, query, safe, lang string, page int, visitedNodes []string) ([]ForumSearchResult, error) {
func sendForumSearchRequestToNode(nodeAddr, query, safe, lang string, page int, visitedNodes []string) ([]ForumSearchResult, error) { // Check if the current node has already been visited
// Check if the current node has already been visited for _, node := range visitedNodes {
for _, node := range visitedNodes { if node == hostID {
if node == hostID { return nil, fmt.Errorf("loop detected: this node (%s) has already been visited", hostID)
return nil, fmt.Errorf("loop detected: this node (%s) has already been visited", hostID) }
} }
}
// Add current node to the list of visited nodes
// Add current node to the list of visited nodes visitedNodes = append(visitedNodes, hostID)
visitedNodes = append(visitedNodes, hostID)
searchParams := struct {
searchParams := struct { Query string `json:"query"`
Query string `json:"query"` Safe string `json:"safe"`
Safe string `json:"safe"` Lang string `json:"lang"`
Lang string `json:"lang"` Page int `json:"page"`
Page int `json:"page"` ResponseAddr string `json:"responseAddr"`
ResponseAddr string `json:"responseAddr"` VisitedNodes []string `json:"visitedNodes"`
VisitedNodes []string `json:"visitedNodes"` }{
}{ Query: query,
Query: query, Safe: safe,
Safe: safe, Lang: lang,
Lang: lang, Page: page,
Page: page, ResponseAddr: fmt.Sprintf("http://localhost:%d/node", config.Port),
ResponseAddr: fmt.Sprintf("http://localhost:%d/node", config.Port), VisitedNodes: visitedNodes,
VisitedNodes: visitedNodes, }
}
msgBytes, err := json.Marshal(searchParams)
msgBytes, err := json.Marshal(searchParams) if err != nil {
if err != nil { return nil, fmt.Errorf("failed to marshal search parameters: %v", err)
return nil, fmt.Errorf("failed to marshal search parameters: %v", err) }
}
msg := Message{
msg := Message{ ID: hostID,
ID: hostID, Type: "search-forum",
Type: "search-forum", Content: string(msgBytes),
Content: string(msgBytes), }
}
err = sendMessage(nodeAddr, msg)
err = sendMessage(nodeAddr, msg) if err != nil {
if err != nil { return nil, fmt.Errorf("failed to send search request to node %s: %v", nodeAddr, err)
return nil, fmt.Errorf("failed to send search request to node %s: %v", nodeAddr, err) }
}
// Wait for results
// Wait for results select {
select { case res := <-forumResultsChan:
case res := <-forumResultsChan: return res, nil
return res, nil case <-time.After(20 * time.Second):
case <-time.After(20 * time.Second): return nil, fmt.Errorf("timeout waiting for results from node %s", nodeAddr)
return nil, fmt.Errorf("timeout waiting for results from node %s", nodeAddr) }
} }
}
func handleForumResultsMessage(msg Message) {
func handleForumResultsMessage(msg Message) { var results []ForumSearchResult
var results []ForumSearchResult err := json.Unmarshal([]byte(msg.Content), &results)
err := json.Unmarshal([]byte(msg.Content), &results) if err != nil {
if err != nil { printWarn("Error unmarshalling forum results: %v", err)
log.Printf("Error unmarshalling forum results: %v", err) return
return }
}
printDebug("Received forum results: %+v", results)
log.Printf("Received forum results: %+v", results) // Send results to forumResultsChan
// Send results to forumResultsChan go func() {
go func() { forumResultsChan <- results
forumResultsChan <- results }()
}() }
}
// Used only to answer requests
// Used only to answer requests func fetchForumResults(query, safe, lang string, page int) []ForumSearchResult {
func fetchForumResults(query, safe, lang string, page int) []ForumSearchResult { results, err := PerformRedditSearch(query, safe, page)
results, err := PerformRedditSearch(query, safe, page) if err != nil {
if err != nil { printWarn("Error fetching forum results: %v", err)
log.Printf("Error fetching forum results: %v", err) return nil
return nil }
} return results
return results }
}

169
node-request-images.go Normal file → Executable file
View file

@ -1,85 +1,84 @@
package main package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "time"
"time" )
)
var imageResultsChan = make(chan []ImageSearchResult)
var imageResultsChan = make(chan []ImageSearchResult)
func handleImageResultsMessage(msg Message) {
func handleImageResultsMessage(msg Message) { var results []ImageSearchResult
var results []ImageSearchResult err := json.Unmarshal([]byte(msg.Content), &results)
err := json.Unmarshal([]byte(msg.Content), &results) if err != nil {
if err != nil { printWarn("Error unmarshalling image results: %v", err)
log.Printf("Error unmarshalling image results: %v", err) return
return }
}
printDebug("Received image results: %+v", results)
log.Printf("Received image results: %+v", results) // Send results to imageResultsChan
// Send results to imageResultsChan go func() {
go func() { imageResultsChan <- results
imageResultsChan <- results }()
}() }
}
func sendImageSearchRequestToNode(nodeAddr, query, safe, lang string, page int, visitedNodes []string) ([]ImageSearchResult, error) {
func sendImageSearchRequestToNode(nodeAddr, query, safe, lang string, page int, visitedNodes []string) ([]ImageSearchResult, error) { visitedNodes = append(visitedNodes, nodeAddr)
visitedNodes = append(visitedNodes, nodeAddr) searchParams := struct {
searchParams := struct { Query string `json:"query"`
Query string `json:"query"` Safe string `json:"safe"`
Safe string `json:"safe"` Lang string `json:"lang"`
Lang string `json:"lang"` Page int `json:"page"`
Page int `json:"page"` ResponseAddr string `json:"responseAddr"`
ResponseAddr string `json:"responseAddr"` VisitedNodes []string `json:"visitedNodes"`
VisitedNodes []string `json:"visitedNodes"` }{
}{ Query: query,
Query: query, Safe: safe,
Safe: safe, Lang: lang,
Lang: lang, Page: page,
Page: page, ResponseAddr: fmt.Sprintf("http://localhost:%d/node", config.Port),
ResponseAddr: fmt.Sprintf("http://localhost:%d/node", config.Port), VisitedNodes: visitedNodes,
VisitedNodes: visitedNodes, }
}
msgBytes, err := json.Marshal(searchParams)
msgBytes, err := json.Marshal(searchParams) if err != nil {
if err != nil { return nil, fmt.Errorf("failed to marshal search parameters: %v", err)
return nil, fmt.Errorf("failed to marshal search parameters: %v", err) }
}
msg := Message{
msg := Message{ ID: hostID,
ID: hostID, Type: "search-image",
Type: "search-image", Content: string(msgBytes),
Content: string(msgBytes), }
}
err = sendMessage(nodeAddr, msg)
err = sendMessage(nodeAddr, msg) if err != nil {
if err != nil { return nil, fmt.Errorf("failed to send search request to node %s: %v", nodeAddr, err)
return nil, fmt.Errorf("failed to send search request to node %s: %v", nodeAddr, err) }
}
// Wait for results
// Wait for results select {
select { case res := <-imageResultsChan:
case res := <-imageResultsChan: return res, nil
return res, nil case <-time.After(30 * time.Second):
case <-time.After(30 * time.Second): return nil, fmt.Errorf("timeout waiting for results from node %s", nodeAddr)
return nil, fmt.Errorf("timeout waiting for results from node %s", nodeAddr) }
} }
}
func tryOtherNodesForImageSearch(query, safe, lang string, page int, visitedNodes []string) []ImageSearchResult {
func tryOtherNodesForImageSearch(query, safe, lang string, page int, visitedNodes []string) []ImageSearchResult { for _, nodeAddr := range peers {
for _, nodeAddr := range peers { if contains(visitedNodes, nodeAddr) {
if contains(visitedNodes, nodeAddr) { continue // Skip nodes already visited
continue // Skip nodes already visited }
} results, err := sendImageSearchRequestToNode(nodeAddr, query, safe, lang, page, visitedNodes)
results, err := sendImageSearchRequestToNode(nodeAddr, query, safe, lang, page, visitedNodes) if err != nil {
if err != nil { printWarn("Error contacting node %s: %v", nodeAddr, err)
log.Printf("Error contacting node %s: %v", nodeAddr, err) continue
continue }
} if len(results) > 0 {
if len(results) > 0 { return results
return results }
} }
} return nil
return nil }
}

169
node-request-text.go Normal file → Executable file
View file

@ -1,85 +1,84 @@
package main package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "time"
"time" )
)
var textResultsChan = make(chan []TextSearchResult)
var textResultsChan = make(chan []TextSearchResult)
func tryOtherNodesForTextSearch(query, safe, lang string, page int, visitedNodes []string) []TextSearchResult {
func tryOtherNodesForTextSearch(query, safe, lang string, page int, visitedNodes []string) []TextSearchResult { for _, nodeAddr := range peers {
for _, nodeAddr := range peers { if contains(visitedNodes, nodeAddr) {
if contains(visitedNodes, nodeAddr) { continue // Skip nodes already visited
continue // Skip nodes already visited }
} results, err := sendTextSearchRequestToNode(nodeAddr, query, safe, lang, page, visitedNodes)
results, err := sendTextSearchRequestToNode(nodeAddr, query, safe, lang, page, visitedNodes) if err != nil {
if err != nil { printWarn("Error contacting node %s: %v", nodeAddr, err)
log.Printf("Error contacting node %s: %v", nodeAddr, err) continue
continue }
} if len(results) > 0 {
if len(results) > 0 { return results
return results }
} }
} return nil
return nil }
}
func sendTextSearchRequestToNode(nodeAddr, query, safe, lang string, page int, visitedNodes []string) ([]TextSearchResult, error) {
func sendTextSearchRequestToNode(nodeAddr, query, safe, lang string, page int, visitedNodes []string) ([]TextSearchResult, error) { visitedNodes = append(visitedNodes, nodeAddr)
visitedNodes = append(visitedNodes, nodeAddr) searchParams := struct {
searchParams := struct { Query string `json:"query"`
Query string `json:"query"` Safe string `json:"safe"`
Safe string `json:"safe"` Lang string `json:"lang"`
Lang string `json:"lang"` Page int `json:"page"`
Page int `json:"page"` ResponseAddr string `json:"responseAddr"`
ResponseAddr string `json:"responseAddr"` VisitedNodes []string `json:"visitedNodes"`
VisitedNodes []string `json:"visitedNodes"` }{
}{ Query: query,
Query: query, Safe: safe,
Safe: safe, Lang: lang,
Lang: lang, Page: page,
Page: page, ResponseAddr: fmt.Sprintf("http://localhost:%d/node", config.Port),
ResponseAddr: fmt.Sprintf("http://localhost:%d/node", config.Port), VisitedNodes: visitedNodes,
VisitedNodes: visitedNodes, }
}
msgBytes, err := json.Marshal(searchParams)
msgBytes, err := json.Marshal(searchParams) if err != nil {
if err != nil { return nil, fmt.Errorf("failed to marshal search parameters: %v", err)
return nil, fmt.Errorf("failed to marshal search parameters: %v", err) }
}
msg := Message{
msg := Message{ ID: hostID,
ID: hostID, Type: "search-text",
Type: "search-text", Content: string(msgBytes),
Content: string(msgBytes), }
}
err = sendMessage(nodeAddr, msg)
err = sendMessage(nodeAddr, msg) if err != nil {
if err != nil { return nil, fmt.Errorf("failed to send search request to node %s: %v", nodeAddr, err)
return nil, fmt.Errorf("failed to send search request to node %s: %v", nodeAddr, err) }
}
// Wait for results
// Wait for results select {
select { case res := <-textResultsChan:
case res := <-textResultsChan: return res, nil
return res, nil case <-time.After(20 * time.Second):
case <-time.After(20 * time.Second): return nil, fmt.Errorf("timeout waiting for results from node %s", nodeAddr)
return nil, fmt.Errorf("timeout waiting for results from node %s", nodeAddr) }
} }
}
func handleTextResultsMessage(msg Message) {
func handleTextResultsMessage(msg Message) { var results []TextSearchResult
var results []TextSearchResult err := json.Unmarshal([]byte(msg.Content), &results)
err := json.Unmarshal([]byte(msg.Content), &results) if err != nil {
if err != nil { printWarn("Error unmarshalling text results: %v", err)
log.Printf("Error unmarshalling text results: %v", err) return
return }
}
printDebug("Received text results: %+v", results)
log.Printf("Received text results: %+v", results) // Send results to textResultsChan
// Send results to textResultsChan go func() {
go func() { textResultsChan <- results
textResultsChan <- results }()
}() }
}

165
node-request-video.go Normal file → Executable file
View file

@ -1,83 +1,82 @@
package main package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "time"
"time" )
)
func tryOtherNodesForVideoSearch(query, safe, lang string, page int, visitedNodes []string) []VideoResult {
func tryOtherNodesForVideoSearch(query, safe, lang string, page int, visitedNodes []string) []VideoResult { for _, nodeAddr := range peers {
for _, nodeAddr := range peers { if contains(visitedNodes, nodeAddr) {
if contains(visitedNodes, nodeAddr) { continue // Skip nodes already visited
continue // Skip nodes already visited }
} results, err := sendVideoSearchRequestToNode(nodeAddr, query, safe, lang, page, visitedNodes)
results, err := sendVideoSearchRequestToNode(nodeAddr, query, safe, lang, page, visitedNodes) if err != nil {
if err != nil { printWarn("Error contacting node %s: %v", nodeAddr, err)
log.Printf("Error contacting node %s: %v", nodeAddr, err) continue
continue }
} if len(results) > 0 {
if len(results) > 0 { return results
return results }
} }
} return nil
return nil }
}
func sendVideoSearchRequestToNode(nodeAddr, query, safe, lang string, page int, visitedNodes []string) ([]VideoResult, error) {
func sendVideoSearchRequestToNode(nodeAddr, query, safe, lang string, page int, visitedNodes []string) ([]VideoResult, error) { visitedNodes = append(visitedNodes, nodeAddr)
visitedNodes = append(visitedNodes, nodeAddr) searchParams := struct {
searchParams := struct { Query string `json:"query"`
Query string `json:"query"` Safe string `json:"safe"`
Safe string `json:"safe"` Lang string `json:"lang"`
Lang string `json:"lang"` Page int `json:"page"`
Page int `json:"page"` ResponseAddr string `json:"responseAddr"`
ResponseAddr string `json:"responseAddr"` VisitedNodes []string `json:"visitedNodes"`
VisitedNodes []string `json:"visitedNodes"` }{
}{ Query: query,
Query: query, Safe: safe,
Safe: safe, Lang: lang,
Lang: lang, Page: page,
Page: page, ResponseAddr: fmt.Sprintf("http://localhost:%d/node", config.Port),
ResponseAddr: fmt.Sprintf("http://localhost:%d/node", config.Port), VisitedNodes: visitedNodes,
VisitedNodes: visitedNodes, }
}
msgBytes, err := json.Marshal(searchParams)
msgBytes, err := json.Marshal(searchParams) if err != nil {
if err != nil { return nil, fmt.Errorf("failed to marshal search parameters: %v", err)
return nil, fmt.Errorf("failed to marshal search parameters: %v", err) }
}
msg := Message{
msg := Message{ ID: hostID,
ID: hostID, Type: "search-video",
Type: "search-video", Content: string(msgBytes),
Content: string(msgBytes), }
}
err = sendMessage(nodeAddr, msg)
err = sendMessage(nodeAddr, msg) if err != nil {
if err != nil { return nil, fmt.Errorf("failed to send search request to node %s: %v", nodeAddr, err)
return nil, fmt.Errorf("failed to send search request to node %s: %v", nodeAddr, err) }
}
// Wait for results
// Wait for results select {
select { case res := <-videoResultsChan:
case res := <-videoResultsChan: return res, nil
return res, nil case <-time.After(20 * time.Second):
case <-time.After(20 * time.Second): return nil, fmt.Errorf("timeout waiting for results from node %s", nodeAddr)
return nil, fmt.Errorf("timeout waiting for results from node %s", nodeAddr) }
} }
}
func handleVideoResultsMessage(msg Message) {
func handleVideoResultsMessage(msg Message) { var results []VideoResult
var results []VideoResult err := json.Unmarshal([]byte(msg.Content), &results)
err := json.Unmarshal([]byte(msg.Content), &results) if err != nil {
if err != nil { printWarn("Error unmarshalling video results: %v", err)
log.Printf("Error unmarshalling video results: %v", err) return
return }
}
printDebug("Received video results: %+v", results)
log.Printf("Received video results: %+v", results) // Send results to videoResultsChan
// Send results to videoResultsChan go func() {
go func() { videoResultsChan <- results
videoResultsChan <- results }()
}() }
}

31
run.bat Executable file
View file

@ -0,0 +1,31 @@
@echo off
setlocal enabledelayedexpansion
rem Directory where the Go files are located
set GO_DIR=C:\path\to\your\go\files
rem Explicitly list the main files in the required order
set FILES=main.go init.go search-engine.go text.go text-google.go text-librex.go text-brave.go text-duckduckgo.go common.go cache.go agent.go files.go files-thepiratebay.go files-torrentgalaxy.go forums.go get-searchxng.go imageproxy.go images.go images-imgur.go images-quant.go map.go node.go open-search.go video.go
rem Change to the directory with the Go files
pushd %GO_DIR%
rem Find all other .go files that were not explicitly listed
set OTHER_GO_FILES=
for %%f in (*.go) do (
set file=%%~nxf
set found=0
for %%i in (%FILES%) do (
if /i "%%i"=="!file!" set found=1
)
if !found!==0 (
set OTHER_GO_FILES=!OTHER_GO_FILES! "%%f"
)
)
rem Run the Go program with the specified files first, followed by the remaining files
go run %FILES% %OTHER_GO_FILES%
rem Return to the original directory
popd

366
text.go Normal file → Executable file
View file

@ -1,183 +1,183 @@
package main package main
import ( import (
"fmt" "fmt"
"html/template" "html/template"
"net/http" "net/http"
"time" "time"
) )
var textSearchEngines []SearchEngine var textSearchEngines []SearchEngine
func init() { func init() {
textSearchEngines = []SearchEngine{ textSearchEngines = []SearchEngine{
{Name: "Google", Func: wrapTextSearchFunc(PerformGoogleTextSearch), Weight: 1}, {Name: "Google", Func: wrapTextSearchFunc(PerformGoogleTextSearch), Weight: 1},
{Name: "LibreX", Func: wrapTextSearchFunc(PerformLibreXTextSearch), Weight: 2}, {Name: "LibreX", Func: wrapTextSearchFunc(PerformLibreXTextSearch), Weight: 2},
{Name: "Brave", Func: wrapTextSearchFunc(PerformBraveTextSearch), Weight: 2}, {Name: "Brave", Func: wrapTextSearchFunc(PerformBraveTextSearch), Weight: 2},
{Name: "DuckDuckGo", Func: wrapTextSearchFunc(PerformDuckDuckGoTextSearch), Weight: 5}, {Name: "DuckDuckGo", Func: wrapTextSearchFunc(PerformDuckDuckGoTextSearch), Weight: 5},
// {Name: "SearXNG", Func: wrapTextSearchFunc(PerformSearXNGTextSearch), Weight: 2}, // Uncomment when implemented // {Name: "SearXNG", Func: wrapTextSearchFunc(PerformSearXNGTextSearch), Weight: 2}, // Uncomment when implemented
} }
} }
func HandleTextSearch(w http.ResponseWriter, settings UserSettings, query, safe, lang string, page int) { func HandleTextSearch(w http.ResponseWriter, settings UserSettings, query string, page int) {
startTime := time.Now() startTime := time.Now()
cacheKey := CacheKey{Query: query, Page: page, Safe: safe == "true", Lang: lang, Type: "text"} cacheKey := CacheKey{Query: query, Page: page, Safe: settings.SafeSearch == "true", Lang: settings.Language, Type: "text"}
combinedResults := getTextResultsFromCacheOrFetch(cacheKey, query, safe, lang, page) combinedResults := getTextResultsFromCacheOrFetch(cacheKey, query, settings.SafeSearch, settings.Language, page)
hasPrevPage := page > 1 // dupe hasPrevPage := page > 1 // dupe
//displayResults(w, combinedResults, query, lang, time.Since(startTime).Seconds(), page, hasPrevPage, hasNextPage) //displayResults(w, combinedResults, query, lang, time.Since(startTime).Seconds(), page, hasPrevPage, hasNextPage)
// Prefetch next and previous pages // Prefetch next and previous pages
go prefetchPage(query, safe, lang, page+1) go prefetchPage(query, settings.SafeSearch, settings.Language, page+1)
if hasPrevPage { if hasPrevPage {
go prefetchPage(query, safe, lang, page-1) go prefetchPage(query, settings.SafeSearch, settings.Language, page-1)
} }
elapsedTime := time.Since(startTime) elapsedTime := time.Since(startTime)
tmpl, err := template.New("text.html").Funcs(funcs).ParseFiles("templates/text.html") tmpl, err := template.New("text.html").Funcs(funcs).ParseFiles("templates/text.html")
if err != nil { if err != nil {
printErr("Error parsing template: %v", err) printErr("Error parsing template: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError) http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return return
} }
data := struct { data := struct {
Results []TextSearchResult Results []TextSearchResult
Query string Query string
Page int Page int
Fetched string Fetched string
LanguageOptions []LanguageOption LanguageOptions []LanguageOption
CurrentLang string CurrentLang string
HasPrevPage bool HasPrevPage bool
HasNextPage bool HasNextPage bool
NoResults bool NoResults bool
Theme string Theme string
}{ }{
Results: combinedResults, Results: combinedResults,
Query: query, Query: query,
Page: page, Page: page,
Fetched: fmt.Sprintf("%.2f seconds", elapsedTime.Seconds()), Fetched: fmt.Sprintf("%.2f seconds", elapsedTime.Seconds()),
LanguageOptions: languageOptions, LanguageOptions: languageOptions,
CurrentLang: lang, CurrentLang: settings.Language,
HasPrevPage: page > 1, HasPrevPage: page > 1,
HasNextPage: len(combinedResults) >= 50, HasNextPage: len(combinedResults) >= 50,
NoResults: len(combinedResults) == 0, NoResults: len(combinedResults) == 0,
Theme: settings.Theme, Theme: settings.Theme,
} }
err = tmpl.Execute(w, data) err = tmpl.Execute(w, data)
if err != nil { if err != nil {
printErr("Error executing template: %v", err) printErr("Error executing template: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError) http.Error(w, "Internal Server Error", http.StatusInternalServerError)
} }
} }
func getTextResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string, page int) []TextSearchResult { func getTextResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string, page int) []TextSearchResult {
cacheChan := make(chan []SearchResult) cacheChan := make(chan []SearchResult)
var combinedResults []TextSearchResult var combinedResults []TextSearchResult
go func() { go func() {
results, exists := resultsCache.Get(cacheKey) results, exists := resultsCache.Get(cacheKey)
if exists { if exists {
printInfo("Cache hit") printInfo("Cache hit")
cacheChan <- results cacheChan <- results
} else { } else {
printInfo("Cache miss") printInfo("Cache miss")
cacheChan <- nil cacheChan <- nil
} }
}() }()
select { select {
case results := <-cacheChan: case results := <-cacheChan:
if results == nil { if results == nil {
combinedResults = fetchTextResults(query, safe, lang, page) combinedResults = fetchTextResults(query, safe, lang, page)
if len(combinedResults) > 0 { if len(combinedResults) > 0 {
resultsCache.Set(cacheKey, convertToSearchResults(combinedResults)) resultsCache.Set(cacheKey, convertToSearchResults(combinedResults))
} }
} else { } else {
textResults, _, _ := convertToSpecificResults(results) textResults, _, _ := convertToSpecificResults(results)
combinedResults = textResults combinedResults = textResults
} }
case <-time.After(2 * time.Second): case <-time.After(2 * time.Second):
printInfo("Cache check timeout") printInfo("Cache check timeout")
combinedResults = fetchTextResults(query, safe, lang, page) combinedResults = fetchTextResults(query, safe, lang, page)
if len(combinedResults) > 0 { if len(combinedResults) > 0 {
resultsCache.Set(cacheKey, convertToSearchResults(combinedResults)) resultsCache.Set(cacheKey, convertToSearchResults(combinedResults))
} }
} }
return combinedResults return combinedResults
} }
func prefetchPage(query, safe, lang string, page int) { func prefetchPage(query, safe, lang string, page int) {
cacheKey := CacheKey{Query: query, Page: page, Safe: safe == "true", Lang: lang, Type: "text"} cacheKey := CacheKey{Query: query, Page: page, Safe: safe == "true", Lang: lang, Type: "text"}
if _, exists := resultsCache.Get(cacheKey); !exists { if _, exists := resultsCache.Get(cacheKey); !exists {
printInfo("Page %d not cached, caching now...", page) printInfo("Page %d not cached, caching now...", page)
pageResults := fetchTextResults(query, safe, lang, page) pageResults := fetchTextResults(query, safe, lang, page)
if len(pageResults) > 0 { if len(pageResults) > 0 {
resultsCache.Set(cacheKey, convertToSearchResults(pageResults)) resultsCache.Set(cacheKey, convertToSearchResults(pageResults))
} }
} else { } else {
printInfo("Page %d already cached", page) printInfo("Page %d already cached", page)
} }
} }
func fetchTextResults(query, safe, lang string, page int) []TextSearchResult { func fetchTextResults(query, safe, lang string, page int) []TextSearchResult {
var results []TextSearchResult var results []TextSearchResult
for _, engine := range textSearchEngines { for _, engine := range textSearchEngines {
printInfo("Using search engine: %s", engine.Name) printInfo("Using search engine: %s", engine.Name)
searchResults, duration, err := engine.Func(query, safe, lang, page) searchResults, duration, err := engine.Func(query, safe, lang, page)
updateEngineMetrics(&engine, duration, err == nil) updateEngineMetrics(&engine, duration, err == nil)
if err != nil { if err != nil {
printWarn("Error performing search with %s: %v", engine.Name, err) printWarn("Error performing search with %s: %v", engine.Name, err)
continue continue
} }
results = append(results, validateResults(searchResults)...) results = append(results, validateResults(searchResults)...)
// If results are found, break out of the loop // If results are found, break out of the loop
if len(results) > 0 { if len(results) > 0 {
break break
} }
} }
// If no results found after trying all engines // If no results found after trying all engines
if len(results) == 0 { if len(results) == 0 {
printWarn("No text results found for query: %s, trying other nodes", query) printWarn("No text results found for query: %s, trying other nodes", query)
results = tryOtherNodesForTextSearch(query, safe, lang, page, []string{hostID}) results = tryOtherNodesForTextSearch(query, safe, lang, page, []string{hostID})
} }
return results return results
} }
func validateResults(searchResults []SearchResult) []TextSearchResult { func validateResults(searchResults []SearchResult) []TextSearchResult {
var validResults []TextSearchResult var validResults []TextSearchResult
// Remove anything that is missing a URL or Header // Remove anything that is missing a URL or Header
for _, result := range searchResults { for _, result := range searchResults {
textResult := result.(TextSearchResult) textResult := result.(TextSearchResult)
if textResult.URL != "" || textResult.Header != "" { if textResult.URL != "" || textResult.Header != "" {
validResults = append(validResults, textResult) validResults = append(validResults, textResult)
} }
} }
return validResults return validResults
} }
func wrapTextSearchFunc(f func(string, string, string, int) ([]TextSearchResult, time.Duration, error)) func(string, string, string, int) ([]SearchResult, time.Duration, error) { func wrapTextSearchFunc(f func(string, string, string, int) ([]TextSearchResult, time.Duration, error)) func(string, string, string, int) ([]SearchResult, time.Duration, error) {
return func(query, safe, lang string, page int) ([]SearchResult, time.Duration, error) { return func(query, safe, lang string, page int) ([]SearchResult, time.Duration, error) {
textResults, duration, err := f(query, safe, lang, page) textResults, duration, err := f(query, safe, lang, page)
if err != nil { if err != nil {
return nil, duration, err return nil, duration, err
} }
searchResults := make([]SearchResult, len(textResults)) searchResults := make([]SearchResult, len(textResults))
for i, result := range textResults { for i, result := range textResults {
searchResults[i] = result searchResults[i] = result
} }
return searchResults, duration, nil return searchResults, duration, nil
} }
} }

422
video.go Normal file → Executable file
View file

@ -1,211 +1,211 @@
package main package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"html/template" "html/template"
"net/http" "net/http"
"net/url" "net/url"
"sync" "sync"
"time" "time"
) )
const retryDuration = 12 * time.Hour // Retry duration for unresponding piped instances const retryDuration = 12 * time.Hour // Retry duration for unresponding piped instances
var ( var (
pipedInstances = []string{ pipedInstances = []string{
"api.piped.yt", "api.piped.yt",
"pipedapi.moomoo.me", "pipedapi.moomoo.me",
"pipedapi.darkness.services", "pipedapi.darkness.services",
"pipedapi.kavin.rocks", "pipedapi.kavin.rocks",
"piped-api.hostux.net", "piped-api.hostux.net",
"pipedapi.syncpundit.io", "pipedapi.syncpundit.io",
"piped-api.cfe.re", "piped-api.cfe.re",
"pipedapi.in.projectsegfau.lt", "pipedapi.in.projectsegfau.lt",
"piapi.ggtyler.dev", "piapi.ggtyler.dev",
"piped-api.codespace.cz", "piped-api.codespace.cz",
"pipedapi.coldforge.xyz", "pipedapi.coldforge.xyz",
"pipedapi.osphost.fi", "pipedapi.osphost.fi",
} }
disabledInstances = make(map[string]bool) disabledInstances = make(map[string]bool)
mu sync.Mutex mu sync.Mutex
videoResultsChan = make(chan []VideoResult) // Channel to receive video results from other nodes videoResultsChan = make(chan []VideoResult) // Channel to receive video results from other nodes
) )
// VideoAPIResponse matches the structure of the JSON response from the Piped API // VideoAPIResponse matches the structure of the JSON response from the Piped API
type VideoAPIResponse struct { type VideoAPIResponse struct {
Items []struct { Items []struct {
URL string `json:"url"` URL string `json:"url"`
Title string `json:"title"` Title string `json:"title"`
UploaderName string `json:"uploaderName"` UploaderName string `json:"uploaderName"`
Views int `json:"views"` Views int `json:"views"`
Thumbnail string `json:"thumbnail"` Thumbnail string `json:"thumbnail"`
Duration int `json:"duration"` Duration int `json:"duration"`
UploadedDate string `json:"uploadedDate"` UploadedDate string `json:"uploadedDate"`
Type string `json:"type"` Type string `json:"type"`
} `json:"items"` } `json:"items"`
} }
// Function to format views similarly to the Python code // Function to format views similarly to the Python code
func formatViews(views int) string { func formatViews(views int) string {
switch { switch {
case views >= 1_000_000_000: case views >= 1_000_000_000:
return fmt.Sprintf("%.1fB views", float64(views)/1_000_000_000) return fmt.Sprintf("%.1fB views", float64(views)/1_000_000_000)
case views >= 1_000_000: case views >= 1_000_000:
return fmt.Sprintf("%.1fM views", float64(views)/1_000_000) return fmt.Sprintf("%.1fM views", float64(views)/1_000_000)
case views >= 10_000: case views >= 10_000:
return fmt.Sprintf("%.1fK views", float64(views)/1_000) return fmt.Sprintf("%.1fK views", float64(views)/1_000)
case views == 1: case views == 1:
return fmt.Sprintf("%d view", views) return fmt.Sprintf("%d view", views)
default: default:
return fmt.Sprintf("%d views", views) return fmt.Sprintf("%d views", views)
} }
} }
// formatDuration formats video duration as done in the Python code // formatDuration formats video duration as done in the Python code
func formatDuration(seconds int) string { func formatDuration(seconds int) string {
if 0 > seconds { if 0 > seconds {
return "Live" return "Live"
} }
hours := seconds / 3600 hours := seconds / 3600
minutes := (seconds % 3600) / 60 minutes := (seconds % 3600) / 60
seconds = seconds % 60 seconds = seconds % 60
if hours > 0 { if hours > 0 {
return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds) return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds)
} }
return fmt.Sprintf("%02d:%02d", minutes, seconds) return fmt.Sprintf("%02d:%02d", minutes, seconds)
} }
func init() { func init() {
go checkDisabledInstancesPeriodically() go checkDisabledInstancesPeriodically()
} }
func checkDisabledInstancesPeriodically() { func checkDisabledInstancesPeriodically() {
checkAndReactivateInstances() // Initial immediate check checkAndReactivateInstances() // Initial immediate check
ticker := time.NewTicker(retryDuration) ticker := time.NewTicker(retryDuration)
defer ticker.Stop() defer ticker.Stop()
for range ticker.C { for range ticker.C {
checkAndReactivateInstances() checkAndReactivateInstances()
} }
} }
func checkAndReactivateInstances() { func checkAndReactivateInstances() {
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
for instance, isDisabled := range disabledInstances { for instance, isDisabled := range disabledInstances {
if isDisabled { if isDisabled {
// Check if the instance is available again // Check if the instance is available again
if testInstanceAvailability(instance) { if testInstanceAvailability(instance) {
printInfo("Instance %s is now available and reactivated.", instance) printInfo("Instance %s is now available and reactivated.", instance)
delete(disabledInstances, instance) delete(disabledInstances, instance)
} else { } else {
printInfo("Instance %s is still not available.", instance) printInfo("Instance %s is still not available.", instance)
} }
} }
} }
} }
func testInstanceAvailability(instance string) bool { func testInstanceAvailability(instance string) bool {
resp, err := http.Get(fmt.Sprintf("https://%s/search?q=%s&filter=all", instance, url.QueryEscape("test"))) resp, err := http.Get(fmt.Sprintf("https://%s/search?q=%s&filter=all", instance, url.QueryEscape("test")))
if err != nil || resp.StatusCode != http.StatusOK { if err != nil || resp.StatusCode != http.StatusOK {
return false return false
} }
return true return true
} }
func makeHTMLRequest(query, safe, lang string, page int) (*VideoAPIResponse, error) { func makeHTMLRequest(query, safe, lang string, page int) (*VideoAPIResponse, error) {
var lastError error var lastError error
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
for _, instance := range pipedInstances { for _, instance := range pipedInstances {
if disabledInstances[instance] { if disabledInstances[instance] {
continue // Skip this instance because it's still disabled continue // Skip this instance because it's still disabled
} }
url := fmt.Sprintf("https://%s/search?q=%s&filter=all&safe=%s&lang=%s&page=%d", instance, url.QueryEscape(query), safe, lang, page) url := fmt.Sprintf("https://%s/search?q=%s&filter=all&safe=%s&lang=%s&page=%d", instance, url.QueryEscape(query), safe, lang, page)
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil || resp.StatusCode != http.StatusOK { if err != nil || resp.StatusCode != http.StatusOK {
printInfo("Disabling instance %s due to error or status code: %v", instance, err) printInfo("Disabling instance %s due to error or status code: %v", instance, err)
disabledInstances[instance] = true disabledInstances[instance] = true
lastError = fmt.Errorf("error making request to %s: %w", instance, err) lastError = fmt.Errorf("error making request to %s: %w", instance, err)
continue continue
} }
defer resp.Body.Close() defer resp.Body.Close()
var apiResp VideoAPIResponse var apiResp VideoAPIResponse
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
lastError = fmt.Errorf("error decoding response from %s: %w", instance, err) lastError = fmt.Errorf("error decoding response from %s: %w", instance, err)
continue continue
} }
return &apiResp, nil return &apiResp, nil
} }
return nil, fmt.Errorf("all instances failed, last error: %v", lastError) return nil, fmt.Errorf("all instances failed, last error: %v", lastError)
} }
// handleVideoSearch adapted from the Python `videoResults`, handles video search requests // handleVideoSearch adapted from the Python `videoResults`, handles video search requests
func handleVideoSearch(w http.ResponseWriter, settings UserSettings, query, safe, lang string, page int) { func handleVideoSearch(w http.ResponseWriter, settings UserSettings, query string, page int) {
start := time.Now() start := time.Now()
results := fetchVideoResults(query, settings.SafeSearch, settings.Language, page) results := fetchVideoResults(query, settings.SafeSearch, settings.Language, page)
if len(results) == 0 { if len(results) == 0 {
printWarn("No results from primary search, trying other nodes") printWarn("No results from primary search, trying other nodes")
results = tryOtherNodesForVideoSearch(query, settings.SafeSearch, settings.Language, page, []string{hostID}) results = tryOtherNodesForVideoSearch(query, settings.SafeSearch, settings.Language, page, []string{hostID})
} }
elapsed := time.Since(start) elapsed := time.Since(start)
tmpl, err := template.New("videos.html").Funcs(funcs).ParseFiles("templates/videos.html") tmpl, err := template.New("videos.html").Funcs(funcs).ParseFiles("templates/videos.html")
if err != nil { if err != nil {
printErr("Error parsing template: %v", err) printErr("Error parsing template: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError) http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return return
} }
err = tmpl.Execute(w, map[string]interface{}{ err = tmpl.Execute(w, map[string]interface{}{
"Results": results, "Results": results,
"Query": query, "Query": query,
"Fetched": fmt.Sprintf("%.2f seconds", elapsed.Seconds()), "Fetched": fmt.Sprintf("%.2f seconds", elapsed.Seconds()),
"Page": page, "Page": page,
"HasPrevPage": page > 1, "HasPrevPage": page > 1,
"HasNextPage": len(results) > 0, // no "HasNextPage": len(results) > 0, // no
"Theme": settings.Theme, "Theme": settings.Theme,
}) })
if err != nil { if err != nil {
printErr("Error executing template: %v", err) printErr("Error executing template: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError) http.Error(w, "Internal Server Error", http.StatusInternalServerError)
} }
} }
func fetchVideoResults(query, safe, lang string, page int) []VideoResult { func fetchVideoResults(query, safe, lang string, page int) []VideoResult {
apiResp, err := makeHTMLRequest(query, safe, lang, page) apiResp, err := makeHTMLRequest(query, safe, lang, page)
if err != nil { if err != nil {
printWarn("Error fetching video results: %v", err) printWarn("Error fetching video results: %v", err)
return nil return nil
} }
var results []VideoResult var results []VideoResult
for _, item := range apiResp.Items { for _, item := range apiResp.Items {
if item.Type == "channel" || item.Type == "playlist" { if item.Type == "channel" || item.Type == "playlist" {
continue continue
} }
if item.UploadedDate == "" { if item.UploadedDate == "" {
item.UploadedDate = "Now" item.UploadedDate = "Now"
} }
results = append(results, VideoResult{ results = append(results, VideoResult{
Href: fmt.Sprintf("https://youtube.com%s", item.URL), Href: fmt.Sprintf("https://youtube.com%s", item.URL),
Title: item.Title, Title: item.Title,
Date: item.UploadedDate, Date: item.UploadedDate,
Views: formatViews(item.Views), Views: formatViews(item.Views),
Creator: item.UploaderName, Creator: item.UploaderName,
Publisher: "Piped", Publisher: "Piped",
Image: fmt.Sprintf("/img_proxy?url=%s", url.QueryEscape(item.Thumbnail)), Image: fmt.Sprintf("/img_proxy?url=%s", url.QueryEscape(item.Thumbnail)),
Duration: formatDuration(item.Duration), Duration: formatDuration(item.Duration),
}) })
} }
return results return results
} }