basic map results + improved fetching from piped

This commit is contained in:
admin 2024-04-15 22:34:50 +02:00
parent 446b8009a2
commit dfbad3da36
10 changed files with 241 additions and 22 deletions

View file

@ -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

View file

@ -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")
}

71
map.go Normal file
View file

@ -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)
}

View file

@ -31,6 +31,10 @@
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="reddit">forum</button>
<button name="t" value="forum" class="clickable">Forums</button>
</div>
<div class="search-container-results-btn">
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="map">map</button>
<button name="t" value="map" class="clickable">Map</button>
</div>
<div class="search-container-results-btn">
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="torrent">share</button>
<button name="t" value="torrent" class="clickable">Torrents</button>

62
templates/map.html Normal file
View file

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ .Query }} - Ocásek</title>
<link rel="stylesheet" href="/static/css/style.css">
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.7.1/dist/leaflet.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.7.1/dist/leaflet.css" />
<style>
body, html {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
#map {
height: 100%;
width: 100%;
}
.no-decoration {
padding: 1px;
border-radius: 5px;
left: 20px;
}
/* Reposition the Leaflet control container */
.leaflet-top.leaflet-left {
top: 70px; /* Adjust this value based on your logo's height */
left: 10px;
}
</style>
</head>
<body>
<form action="/search" id="prev-next-form" class="results-search-container" method="GET" autocomplete="off">
<h1 class="logomobile"><a class="no-decoration" href="./">Ocásek</a></h1>
<div class="wrapper-results">
<input type="text" name="q" value="{{ .Query }}" id="search-input" placeholder="Type to search..." />
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="map">search</button>
<input type="submit" class="hide" name="t" value="map" />
</div>
</form>
<div id="map"></div>
<script>
var map = L.map('map').setView([50.0755, 14.4378], 13); // Default to Prague, Czech Republic
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap contributors'
}).addTo(map);
// Use the passed coordinates to update the map
var latitude = {{ .Latitude }};
var longitude = {{ .Longitude }};
function updateMap(latitude, longitude) {
map.setView([latitude, longitude], 13); // Set view to new coordinates
}
updateMap(latitude, longitude); // Update the map view to the new location
</script>
</body>
</html>

View file

@ -8,14 +8,14 @@
</head>
<body>
<div class="settings-search-div settings-search-div-search">
<a id="openSettings" class="material-symbols-outlined" href="/settings">tune</a>
<a class="material-icons-round clickable" href="/settings">tune</a>
</div>
<form action="/search" class="search-container" method="post" autocomplete="off">
<h1>Ocásek</h1>
<div class="wrapper">
<input type="text" name="q" autofocus id="search-input" placeholder="Type to search..." />
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="text" type="submit">search</button>
<a id="clearSearch" class="material-symbols-outline">close</a>
<!-- <a id="clearSearch" class="material-icons-round">close</a> -->
</div>
<div class="search-button-wrapper">
<input type="hidden" name="p" value="1">

View file

@ -3,14 +3,14 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Settings</title>
<title>Settings - Ocásek</title>
<link rel="stylesheet" type="text/css" href="static/css/style.css">
</head>
<body>
<form action="/search" id="prev-next-form" class="results-search-container" method="GET" autocomplete="off">
<h1 class="logomobile"><a class="no-decoration" href="./">Ocásek</a></h1>
<div class="wrapper-results">
<input type="text" name="q" value="{{ .Query }}" id="search-input" placeholder="Type to search..." />
<input type="text" name="q" value="" id="search-input" placeholder="Type to search..." />
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="text">search</button>
<input type="submit" class="hide" name="t" value="text" />
</div>

View file

@ -31,6 +31,10 @@
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="reddit">forum</button>
<button name="t" value="forum" class="clickable">Forums</button>
</div>
<div class="search-container-results-btn">
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="map">map</button>
<button name="t" value="map" class="clickable">Map</button>
</div>
<div class="search-container-results-btn">
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="torrent">share</button>
<button name="t" value="torrent" class="clickable">Torrents</button>
@ -49,7 +53,7 @@
<option value="{{.Code}}" {{if eq .Code $.CurrentLang}}selected{{end}}>{{.Name}}</option>
{{end}}
</select>
<button class="results-save" type="submit">Apply settings</button>
<button class="results-save" name="t" value="text">Apply settings</button>
</form>
<div class="results">
<!-- Results go here -->

View file

@ -31,6 +31,10 @@
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="reddit">forum</button>
<button name="t" value="forum" class="clickable">Forums</button>
</div>
<div class="search-container-results-btn">
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="map">map</button>
<button name="t" value="map" class="clickable">Map</button>
</div>
<div class="search-container-results-btn">
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="torrent">share</button>
<button name="t" value="torrent" class="clickable">Torrents</button>

102
video.go
View file

@ -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) {