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