2025-04-18 11:22:42 +02:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
)
|
|
|
|
|
|
|
|
type MusicAPIResponse struct {
|
|
|
|
Items []struct {
|
|
|
|
Title string `json:"title"`
|
|
|
|
UploaderName string `json:"uploaderName"`
|
|
|
|
Duration int `json:"duration"`
|
|
|
|
Thumbnail string `json:"thumbnail"`
|
|
|
|
URL string `json:"url"`
|
|
|
|
} `json:"items"` // Removed VideoID since we'll parse from URL
|
|
|
|
}
|
|
|
|
|
|
|
|
func SearchMusicViaPiped(query string, page int) ([]MusicResult, error) {
|
|
|
|
var lastError error
|
2025-06-10 21:28:40 +02:00
|
|
|
|
|
|
|
// We will try to use preferred instance
|
2025-04-18 11:22:42 +02:00
|
|
|
mu.Lock()
|
2025-06-10 21:28:40 +02:00
|
|
|
instance := preferredInstance
|
|
|
|
mu.Unlock()
|
|
|
|
|
|
|
|
if instance != "" && !disabledInstances[instance] {
|
|
|
|
url := fmt.Sprintf(
|
|
|
|
"https://%s/search?q=%s&filter=music_songs&page=%d",
|
|
|
|
instance,
|
|
|
|
url.QueryEscape(query),
|
|
|
|
page,
|
|
|
|
)
|
2025-04-18 11:22:42 +02:00
|
|
|
|
2025-06-10 21:28:40 +02:00
|
|
|
resp, err := http.Get(url)
|
|
|
|
if err == nil && resp.StatusCode == http.StatusOK {
|
|
|
|
defer resp.Body.Close()
|
|
|
|
var apiResp MusicAPIResponse
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err == nil {
|
|
|
|
return convertPipedToMusicResults(instance, apiResp), nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
printWarn("Preferred instance %s failed for music, falling back", instance)
|
|
|
|
disableInstance(instance)
|
|
|
|
}
|
|
|
|
|
|
|
|
// 2. Fallback using others
|
|
|
|
mu.Lock()
|
|
|
|
defer mu.Unlock()
|
|
|
|
for _, inst := range pipedInstances {
|
|
|
|
if disabledInstances[inst] {
|
2025-04-18 11:22:42 +02:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
url := fmt.Sprintf(
|
|
|
|
"https://%s/search?q=%s&filter=music_songs&page=%d",
|
2025-06-10 21:28:40 +02:00
|
|
|
inst,
|
2025-04-18 11:22:42 +02:00
|
|
|
url.QueryEscape(query),
|
|
|
|
page,
|
|
|
|
)
|
|
|
|
|
|
|
|
resp, err := http.Get(url)
|
|
|
|
if err != nil || resp.StatusCode != http.StatusOK {
|
2025-06-10 21:28:40 +02:00
|
|
|
printInfo("Disabling instance %s due to error: %v", inst, err)
|
|
|
|
disabledInstances[inst] = true
|
|
|
|
lastError = fmt.Errorf("request to %s failed: %w", inst, err)
|
2025-04-18 11:22:42 +02:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
var apiResp MusicAPIResponse
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
|
2025-06-10 21:28:40 +02:00
|
|
|
lastError = fmt.Errorf("failed to decode response from %s: %w", inst, err)
|
2025-04-18 11:22:42 +02:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2025-06-10 21:28:40 +02:00
|
|
|
preferredInstance = inst
|
|
|
|
return convertPipedToMusicResults(inst, apiResp), nil
|
2025-04-18 11:22:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("all Piped instances failed, last error: %v", lastError)
|
|
|
|
}
|
|
|
|
|
|
|
|
func convertPipedToMusicResults(instance string, resp MusicAPIResponse) []MusicResult {
|
|
|
|
seen := make(map[string]bool)
|
|
|
|
var results []MusicResult
|
|
|
|
|
|
|
|
for _, item := range resp.Items {
|
|
|
|
// Extract video ID from URL
|
|
|
|
u, err := url.Parse(item.URL)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
videoID := u.Query().Get("v")
|
|
|
|
if videoID == "" || seen[videoID] {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
seen[videoID] = true
|
|
|
|
|
|
|
|
results = append(results, MusicResult{
|
|
|
|
Title: item.Title,
|
|
|
|
Artist: item.UploaderName,
|
|
|
|
URL: fmt.Sprintf("https://music.youtube.com%s", item.URL),
|
|
|
|
Duration: formatDuration(item.Duration),
|
|
|
|
Thumbnail: item.Thumbnail,
|
|
|
|
Source: "YouTube Music",
|
|
|
|
//AudioURL: fmt.Sprintf("https://%s/stream/%s", instance, videoID),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return results
|
|
|
|
}
|