basic map results + improved fetching from piped
This commit is contained in:
parent
1efca320c8
commit
6c9ec56327
9 changed files with 239 additions and 20 deletions
2
main.go
2
main.go
|
@ -109,6 +109,8 @@ func handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||||
handleImageSearch(w, query, safe, lang, page)
|
handleImageSearch(w, query, safe, lang, page)
|
||||||
case "video":
|
case "video":
|
||||||
videoSearchEndpointHandler(w, r)
|
videoSearchEndpointHandler(w, r)
|
||||||
|
case "map":
|
||||||
|
handleMapSearch(w, query, safe) // implement map results
|
||||||
default:
|
default:
|
||||||
http.ServeFile(w, r, "templates/search.html")
|
http.ServeFile(w, r, "templates/search.html")
|
||||||
}
|
}
|
||||||
|
|
71
map.go
Normal file
71
map.go
Normal 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)
|
||||||
|
}
|
|
@ -31,6 +31,10 @@
|
||||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="reddit">forum</button>
|
<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>
|
<button name="t" value="forum" class="clickable">Forums</button>
|
||||||
</div>
|
</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">
|
<div class="search-container-results-btn">
|
||||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="torrent">share</button>
|
<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>
|
<button name="t" value="torrent" class="clickable">Torrents</button>
|
||||||
|
|
62
templates/map.html
Normal file
62
templates/map.html
Normal 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>
|
|
@ -8,14 +8,14 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="settings-search-div settings-search-div-search">
|
<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>
|
</div>
|
||||||
<form action="/search" class="search-container" method="post" autocomplete="off">
|
<form action="/search" class="search-container" method="post" autocomplete="off">
|
||||||
<h1>Ocásek</h1>
|
<h1>Ocásek</h1>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<input type="text" name="q" autofocus id="search-input" placeholder="Type to search..." />
|
<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>
|
<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>
|
||||||
<div class="search-button-wrapper">
|
<div class="search-button-wrapper">
|
||||||
<input type="hidden" name="p" value="1">
|
<input type="hidden" name="p" value="1">
|
||||||
|
|
|
@ -3,14 +3,14 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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">
|
<link rel="stylesheet" type="text/css" href="static/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<form action="/search" id="prev-next-form" class="results-search-container" method="GET" autocomplete="off">
|
<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>
|
<h1 class="logomobile"><a class="no-decoration" href="./">Ocásek</a></h1>
|
||||||
<div class="wrapper-results">
|
<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>
|
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="text">search</button>
|
||||||
<input type="submit" class="hide" name="t" value="text" />
|
<input type="submit" class="hide" name="t" value="text" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -31,6 +31,10 @@
|
||||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="reddit">forum</button>
|
<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>
|
<button name="t" value="forum" class="clickable">Forums</button>
|
||||||
</div>
|
</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">
|
<div class="search-container-results-btn">
|
||||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="torrent">share</button>
|
<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>
|
<button name="t" value="torrent" class="clickable">Torrents</button>
|
||||||
|
@ -49,7 +53,7 @@
|
||||||
<option value="{{.Code}}" {{if eq .Code $.CurrentLang}}selected{{end}}>{{.Name}}</option>
|
<option value="{{.Code}}" {{if eq .Code $.CurrentLang}}selected{{end}}>{{.Name}}</option>
|
||||||
{{end}}
|
{{end}}
|
||||||
</select>
|
</select>
|
||||||
<button class="results-save" type="submit">Apply settings</button>
|
<button class="results-save" name="t" value="text">Apply settings</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="results">
|
<div class="results">
|
||||||
<!-- Results go here -->
|
<!-- Results go here -->
|
||||||
|
|
|
@ -31,6 +31,10 @@
|
||||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="reddit">forum</button>
|
<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>
|
<button name="t" value="forum" class="clickable">Forums</button>
|
||||||
</div>
|
</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">
|
<div class="search-container-results-btn">
|
||||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="torrent">share</button>
|
<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>
|
<button name="t" value="torrent" class="clickable">Torrents</button>
|
||||||
|
|
102
video.go
102
video.go
|
@ -8,10 +8,30 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
"time"
|
"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
|
// VideoResult reflects the structured data for a video result
|
||||||
type VideoResult struct {
|
type VideoResult struct {
|
||||||
|
@ -71,24 +91,76 @@ func formatDuration(seconds int) string {
|
||||||
return fmt.Sprintf("%02d:%02d", minutes, seconds)
|
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) {
|
func makeHTMLRequest(query string) (*VideoAPIResponse, error) {
|
||||||
resp, err := http.Get(fmt.Sprintf("https://%s/search?q=%s&filter=all", PIPED_INSTANCE, url.QueryEscape(query)))
|
var lastError error
|
||||||
if err != nil {
|
mu.Lock()
|
||||||
return nil, fmt.Errorf("error making request: %w", err)
|
defer mu.Unlock()
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
for _, instance := range pipedInstances {
|
||||||
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
if disabledInstances[instance] {
|
||||||
}
|
continue // Skip this instance because it's still disabled
|
||||||
|
}
|
||||||
|
|
||||||
var apiResp VideoAPIResponse
|
url := fmt.Sprintf("https://%s/search?q=%s&filter=all", instance, url.QueryEscape(query))
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
|
resp, err := http.Get(url)
|
||||||
return nil, fmt.Errorf("error decoding response: %w", err)
|
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) {
|
func videoSearchEndpointHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue