cleanup
This commit is contained in:
parent
cdc4c60618
commit
8fece91f75
16 changed files with 2275 additions and 2252 deletions
422
video.go
Normal file → Executable file
422
video.go
Normal file → Executable file
|
@ -1,211 +1,211 @@
|
|||
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, safe, lang 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{}{
|
||||
"Results": results,
|
||||
"Query": query,
|
||||
"Fetched": fmt.Sprintf("%.2f seconds", elapsed.Seconds()),
|
||||
"Page": page,
|
||||
"HasPrevPage": page > 1,
|
||||
"HasNextPage": len(results) > 0, // no
|
||||
"Theme": settings.Theme,
|
||||
})
|
||||
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",
|
||||
Image: fmt.Sprintf("/img_proxy?url=%s", url.QueryEscape(item.Thumbnail)),
|
||||
Duration: formatDuration(item.Duration),
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
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{}{
|
||||
"Results": results,
|
||||
"Query": query,
|
||||
"Fetched": fmt.Sprintf("%.2f seconds", elapsed.Seconds()),
|
||||
"Page": page,
|
||||
"HasPrevPage": page > 1,
|
||||
"HasNextPage": len(results) > 0, // no
|
||||
"Theme": settings.Theme,
|
||||
})
|
||||
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",
|
||||
Image: fmt.Sprintf("/img_proxy?url=%s", url.QueryEscape(item.Thumbnail)),
|
||||
Duration: formatDuration(item.Duration),
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue