diff --git a/README.md b/README.md index f6f0038..016321d 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,12 @@ - [x] Text results - [x] Image results - [X] Video results +- [X] Map results - [x] HTML+CSS site (no JS version) ## Pending Tasks - [ ] Forums results -- [ ] Map results - [ ] Torrent results - [ ] Website with JS version - [ ] JS applets for results (such as calculator) @@ -21,7 +21,7 @@ # Go Search Engine -A self-hosted aggregate search engine that respects privacy, contains no ads, and serves as a proxy/alternative to Google website. +A self-hosted [metasearch engine](https://en.wikipedia.org/wiki/Metasearch_engine) that respects privacy, contains no ads, and serves as a proxy/alternative to Google website. ## Features diff --git a/main.go b/main.go index 9550f84..ab059fb 100644 --- a/main.go +++ b/main.go @@ -109,6 +109,8 @@ func handleSearch(w http.ResponseWriter, r *http.Request) { handleImageSearch(w, query, safe, lang, page) case "video": videoSearchEndpointHandler(w, r) + case "map": + handleMapSearch(w, query, safe) // implement map results default: http.ServeFile(w, r, "templates/search.html") } diff --git a/map.go b/map.go new file mode 100644 index 0000000..c260fa7 --- /dev/null +++ b/map.go @@ -0,0 +1,71 @@ +package main + +import ( + "encoding/json" + "fmt" + "html/template" + "log" + "net/http" + "net/url" +) + +type NominatimResponse struct { + Lat string `json:"lat"` + Lon string `json:"lon"` +} + +func geocodeQuery(query string) (latitude, longitude string, err error) { + // URL encode the query + query = url.QueryEscape(query) + + // Construct the request URL + urlString := fmt.Sprintf("https://nominatim.openstreetmap.org/search?format=json&q=%s", query) + + // Make the HTTP GET request + resp, err := http.Get(urlString) + if err != nil { + return "", "", err + } + defer resp.Body.Close() + + // Read the response + var result []NominatimResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return "", "", err + } + + // Check if there are any results + if len(result) > 0 { + latitude = result[0].Lat + longitude = result[0].Lon + return latitude, longitude, nil + } + + return "", "", fmt.Errorf("no results found") +} + +func handleMapSearch(w http.ResponseWriter, query string, lang string) { + // Geocode the query to get coordinates + latitude, longitude, err := geocodeQuery(query) + if err != nil { + log.Printf("Error geocoding query: %s, error: %v", query, err) + http.Error(w, "Failed to find location", http.StatusInternalServerError) + return + } + + // Use a template to serve the map page + data := map[string]interface{}{ + "Query": query, + "Latitude": latitude, + "Longitude": longitude, + } + + tmpl, err := template.ParseFiles("templates/map.html") + if err != nil { + log.Printf("Error loading map template: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + tmpl.Execute(w, data) +} diff --git a/templates/images.html b/templates/images.html index 0eb14d2..2f38aab 100644 --- a/templates/images.html +++ b/templates/images.html @@ -31,6 +31,10 @@ +
+ + +
diff --git a/templates/map.html b/templates/map.html new file mode 100644 index 0000000..b8b5499 --- /dev/null +++ b/templates/map.html @@ -0,0 +1,62 @@ + + + + + + {{ .Query }} - Ocásek + + + + + + +
+

Ocásek

+
+ + + +
+
+
+ + + diff --git a/templates/search.html b/templates/search.html index 710f381..368540a 100644 --- a/templates/search.html +++ b/templates/search.html @@ -8,14 +8,14 @@

Ocásek

- close +
diff --git a/templates/settings.html b/templates/settings.html index c7540b2..80abb19 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -3,14 +3,14 @@ - Settings + Settings - Ocásek

Ocásek

- +
diff --git a/templates/text.html b/templates/text.html index 4d188aa..461861f 100644 --- a/templates/text.html +++ b/templates/text.html @@ -31,6 +31,10 @@
+
+ + +
@@ -49,7 +53,7 @@ {{end}} - +
diff --git a/templates/videos.html b/templates/videos.html index f4fcc80..3f1fb07 100644 --- a/templates/videos.html +++ b/templates/videos.html @@ -31,6 +31,10 @@
+
+ + +
diff --git a/video.go b/video.go index dc524d1..dc8c89b 100644 --- a/video.go +++ b/video.go @@ -8,10 +8,30 @@ import ( "net/http" "net/url" "strconv" + "sync" "time" ) -const PIPED_INSTANCE = "pipedapi.kavin.rocks" +const retryDuration = 12 * time.Hour // Retry duration for unresponding piped instances + +var ( + pipedInstances = []string{ + "pipedapi.kavin.rocks", + "api.piped.yt", + "pipedapi.moomoo.me", + "pipedapi.darkness.services", + "piped-api.hostux.net", + "pipedapi.syncpundit.io", + "piped-api.cfe.re", + "pipedapi.in.projectsegfau.lt", + "piapi.ggtyler.dev", + "piped-api.codespace.cz", + "pipedapi.coldforge.xyz", + "pipedapi.osphost.fi", + } + disabledInstances = make(map[string]bool) + mu sync.Mutex +) // VideoResult reflects the structured data for a video result type VideoResult struct { @@ -71,24 +91,76 @@ func formatDuration(seconds int) string { return fmt.Sprintf("%02d:%02d", minutes, seconds) } -// makeHTMLRequest fetches search results from the Piped API, similarly to the Python `makeHTMLRequest` +func init() { + go checkDisabledInstancesPeriodically() +} + +func checkDisabledInstancesPeriodically() { + checkAndReactivateInstances() // Initial immediate check + ticker := time.NewTicker(retryDuration) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + checkAndReactivateInstances() + } + } +} + +func checkAndReactivateInstances() { + mu.Lock() + defer mu.Unlock() + + for instance, isDisabled := range disabledInstances { + if isDisabled { + // Check if the instance is available again + if testInstanceAvailability(instance) { + log.Printf("Instance %s is now available and reactivated.", instance) + delete(disabledInstances, instance) + } else { + log.Printf("Instance %s is still not available.", instance) + } + } + } +} + +func testInstanceAvailability(instance string) bool { + resp, err := http.Get(fmt.Sprintf("https://%s/search?q=%s&filter=all", instance, url.QueryEscape("test"))) + if err != nil || resp.StatusCode != http.StatusOK { + return false + } + return true +} + func makeHTMLRequest(query string) (*VideoAPIResponse, error) { - resp, err := http.Get(fmt.Sprintf("https://%s/search?q=%s&filter=all", PIPED_INSTANCE, url.QueryEscape(query))) - if err != nil { - return nil, fmt.Errorf("error making request: %w", err) - } - defer resp.Body.Close() + var lastError error + mu.Lock() + defer mu.Unlock() - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode) - } + for _, instance := range pipedInstances { + if disabledInstances[instance] { + continue // Skip this instance because it's still disabled + } - var apiResp VideoAPIResponse - if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { - return nil, fmt.Errorf("error decoding response: %w", err) - } + url := fmt.Sprintf("https://%s/search?q=%s&filter=all", instance, url.QueryEscape(query)) + resp, err := http.Get(url) + if err != nil || resp.StatusCode != http.StatusOK { + log.Printf("Disabling instance %s due to error or status code: %v", instance, err) + disabledInstances[instance] = true + lastError = fmt.Errorf("error making request to %s: %w", instance, err) + continue + } - return &apiResp, nil + defer resp.Body.Close() + var apiResp VideoAPIResponse + if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { + lastError = fmt.Errorf("error decoding response from %s: %w", instance, err) + continue + } + return &apiResp, nil + } + return nil, fmt.Errorf("all instances failed, last error: %v", lastError) } func videoSearchEndpointHandler(w http.ResponseWriter, r *http.Request) {