2024-08-13 16:31:28 +02:00
package main
import (
"encoding/json"
"fmt"
"html/template"
"net/http"
"net/url"
"sync"
"time"
)
const retryDuration = 12 * time . Hour // Retry duration for unresponding piped instances
var (
pipedInstances = [ ] string {
"api.piped.yt" ,
"pipedapi.moomoo.me" ,
"pipedapi.darkness.services" ,
"pipedapi.kavin.rocks" ,
"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 ) {
printInfo ( "Instance %s is now available and reactivated." , instance )
delete ( disabledInstances , instance )
} else {
printInfo ( "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 {
printInfo ( "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 , settings UserSettings , query string , page int ) {
start := time . Now ( )
results := fetchVideoResults ( query , settings . SafeSearch , settings . Language , page )
if len ( results ) == 0 {
printWarn ( "No results from primary search, trying other nodes" )
results = tryOtherNodesForVideoSearch ( query , settings . SafeSearch , settings . Language , page , [ ] string { hostID } )
}
elapsed := time . Since ( start )
tmpl , err := template . New ( "videos.html" ) . Funcs ( funcs ) . ParseFiles ( "templates/videos.html" )
if err != nil {
printErr ( "Error parsing template: %v" , err )
http . Error ( w , "Internal Server Error" , http . StatusInternalServerError )
return
}
err = tmpl . Execute ( w , map [ string ] interface { } {
2024-08-13 16:38:02 +02:00
"Results" : results ,
"Query" : query ,
"Fetched" : fmt . Sprintf ( "%.2f seconds" , elapsed . Seconds ( ) ) ,
"Page" : page ,
"HasPrevPage" : page > 1 ,
"HasNextPage" : len ( results ) > 0 ,
"LanguageOptions" : languageOptions ,
"CurrentLang" : settings . Language ,
"Theme" : settings . Theme ,
"Safe" : settings . SafeSearch ,
2024-08-28 21:31:27 +02:00
"IsThemeDark" : settings . IsThemeDark ,
2024-08-13 16:31:28 +02:00
} )
if err != nil {
printErr ( "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 {
printWarn ( "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" ,
2024-08-20 18:40:04 +02:00
Image : item . Thumbnail , //fmt.Sprintf("/img_proxy?url=%s", url.QueryEscape(item.Thumbnail)), // Using image proxy is not working, but its not needed here as piped is proxy anyway
2024-08-13 16:31:28 +02:00
Duration : formatDuration ( item . Duration ) ,
} )
}
return results
}