Search/video.go
2024-08-08 21:59:10 +02:00

232 lines
6.4 KiB
Go

package main
import (
"encoding/json"
"fmt"
"html/template"
"log"
"net/http"
"net/url"
"sync"
"time"
)
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
)
// VideoAPIResponse matches the structure of the JSON response from the Piped API
type VideoAPIResponse struct {
Items []struct {
URL string `json:"url"`
Title string `json:"title"`
UploaderName string `json:"uploaderName"`
Views int `json:"views"`
Thumbnail string `json:"thumbnail"`
Duration int `json:"duration"`
UploadedDate string `json:"uploadedDate"`
Type string `json:"type"`
} `json:"items"`
}
// Function to format views similarly to the Python code
func formatViews(views int) string {
switch {
case views >= 1_000_000_000:
return fmt.Sprintf("%.1fB views", float64(views)/1_000_000_000)
case views >= 1_000_000:
return fmt.Sprintf("%.1fM views", float64(views)/1_000_000)
case views >= 10_000:
return fmt.Sprintf("%.1fK views", float64(views)/1_000)
case views == 1:
return fmt.Sprintf("%d view", views)
default:
return fmt.Sprintf("%d views", views)
}
}
// formatDuration formats video duration as done in the Python code
func formatDuration(seconds int) string {
if 0 > seconds {
return "Live"
}
hours := seconds / 3600
minutes := (seconds % 3600) / 60
seconds = seconds % 60
if hours > 0 {
return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds)
}
return fmt.Sprintf("%02d:%02d", minutes, seconds)
}
func init() {
go checkDisabledInstancesPeriodically()
}
func checkDisabledInstancesPeriodically() {
checkAndReactivateInstances() // Initial immediate check
ticker := time.NewTicker(retryDuration)
defer ticker.Stop()
for range 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, safe, lang string, page int) (*VideoAPIResponse, error) {
var lastError error
mu.Lock()
defer mu.Unlock()
for _, instance := range pipedInstances {
if disabledInstances[instance] {
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)
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
}
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)
}
// handleVideoSearch adapted from the Python `videoResults`, handles video search requests
func handleVideoSearch(w http.ResponseWriter, query, safe, lang string, page int) {
start := time.Now()
apiResp, err := makeHTMLRequest(query, safe, lang, page)
if err != nil {
log.Printf("Error fetching video results: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
var results []VideoResult
for _, item := range apiResp.Items {
if item.Type == "channel" || item.Type == "playlist" {
continue
}
if item.UploadedDate == "" {
item.UploadedDate = "Now"
}
results = append(results, VideoResult{
Href: fmt.Sprintf("https://youtube.com%s", item.URL),
Title: item.Title,
Date: item.UploadedDate,
Views: formatViews(item.Views),
Creator: item.UploaderName,
Publisher: "Piped",
Image: fmt.Sprintf("/img_proxy?url=%s", url.QueryEscape(item.Thumbnail)),
Duration: formatDuration(item.Duration),
})
}
elapsed := time.Since(start)
tmpl, err := template.New("videos.html").Funcs(funcs).ParseFiles("templates/videos.html")
if err != nil {
log.Printf("Error parsing template: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
err = tmpl.Execute(w, map[string]interface{}{
"Results": results,
"Query": query,
"Fetched": fmt.Sprintf("%.2f seconds", elapsed.Seconds()),
"Page": page,
"HasPrevPage": page > 1,
"HasNextPage": len(results) > 0, // assuming you have a way to determine if there are more pages
})
if err != nil {
log.Printf("Error executing template: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
func fetchVideoResults(query, safe, lang string, page int) []VideoResult {
apiResp, err := makeHTMLRequest(query, safe, lang, page)
if err != nil {
log.Printf("Error fetching video results: %v", err)
return nil
}
var results []VideoResult
for _, item := range apiResp.Items {
if item.Type == "channel" || item.Type == "playlist" {
continue
}
if item.UploadedDate == "" {
item.UploadedDate = "Now"
}
results = append(results, VideoResult{
Href: fmt.Sprintf("https://youtube.com%s", item.URL),
Title: item.Title,
Date: item.UploadedDate,
Views: formatViews(item.Views),
Creator: item.UploaderName,
Publisher: "Piped",
Image: fmt.Sprintf("/img_proxy?url=%s", url.QueryEscape(item.Thumbnail)),
Duration: formatDuration(item.Duration),
})
}
return results
}