Cache preferred Piped instance to furde reduce start delay
Some checks failed
Run Integration Tests / test (push) Has been cancelled

This commit is contained in:
partisan 2025-06-10 21:28:40 +02:00
parent 8f31f0b2eb
commit be973266c6
2 changed files with 120 additions and 16 deletions

View file

@ -19,14 +19,13 @@ type MusicAPIResponse struct {
func SearchMusicViaPiped(query string, page int) ([]MusicResult, error) { func SearchMusicViaPiped(query string, page int) ([]MusicResult, error) {
var lastError error var lastError error
// We will try to use preferred instance
mu.Lock() mu.Lock()
defer mu.Unlock() instance := preferredInstance
mu.Unlock()
for _, instance := range pipedInstances {
if disabledInstances[instance] {
continue
}
if instance != "" && !disabledInstances[instance] {
url := fmt.Sprintf( url := fmt.Sprintf(
"https://%s/search?q=%s&filter=music_songs&page=%d", "https://%s/search?q=%s&filter=music_songs&page=%d",
instance, instance,
@ -34,22 +33,51 @@ func SearchMusicViaPiped(query string, page int) ([]MusicResult, error) {
page, page,
) )
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] {
continue
}
url := fmt.Sprintf(
"https://%s/search?q=%s&filter=music_songs&page=%d",
inst,
url.QueryEscape(query),
page,
)
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil || resp.StatusCode != http.StatusOK { if err != nil || resp.StatusCode != http.StatusOK {
printInfo("Disabling instance %s due to error: %v", instance, err) printInfo("Disabling instance %s due to error: %v", inst, err)
disabledInstances[instance] = true disabledInstances[inst] = true
lastError = fmt.Errorf("request to %s failed: %w", instance, err) lastError = fmt.Errorf("request to %s failed: %w", inst, err)
continue continue
} }
defer resp.Body.Close() defer resp.Body.Close()
var apiResp MusicAPIResponse var apiResp MusicAPIResponse
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
lastError = fmt.Errorf("failed to decode response from %s: %w", instance, err) lastError = fmt.Errorf("failed to decode response from %s: %w", inst, err)
continue continue
} }
return convertPipedToMusicResults(instance, apiResp), nil preferredInstance = inst
return convertPipedToMusicResults(inst, apiResp), nil
} }
return nil, fmt.Errorf("all Piped instances failed, last error: %v", lastError) return nil, fmt.Errorf("all Piped instances failed, last error: %v", lastError)

View file

@ -5,6 +5,9 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"os"
"path/filepath"
"strings"
"sync" "sync"
"time" "time"
) )
@ -12,18 +15,90 @@ import (
const retryDuration = 12 * time.Hour // Retry duration for unresponding piped instances const retryDuration = 12 * time.Hour // Retry duration for unresponding piped instances
var ( var (
preferredInstance string preferredInstance string
pipedInstances = []string{} pipedInstanceCacheFile string
disabledInstances = make(map[string]bool) pipedInstances = []string{}
mu sync.Mutex disabledInstances = make(map[string]bool)
mu sync.Mutex
) )
func initPipedInstances() { func initPipedInstances() {
pipedInstances = config.MetaSearch.Video if config.DriveCacheEnabled {
pipedInstanceCacheFile = filepath.Join(config.DriveCache.Path, "piped_instances.txt")
cached := loadCachedPipedInstances()
if len(cached) > 0 {
pipedInstances = cached
printInfo("Loaded %d cached Piped instances from disk.", len(cached))
} else {
pipedInstances = config.MetaSearch.Video
savePipedInstances(pipedInstances)
}
// load preferred
if pref := loadPreferredInstance(); pref != "" {
preferredInstance = pref
printInfo("Using cached preferred Piped instance: %s", pref)
}
} else {
pipedInstances = config.MetaSearch.Video
}
go checkDisabledInstancesPeriodically() go checkDisabledInstancesPeriodically()
go selectInitialPipedInstance() go selectInitialPipedInstance()
} }
func savePreferredInstance(instance string) {
if pipedInstanceCacheFile == "" {
return
}
path := filepath.Join(filepath.Dir(pipedInstanceCacheFile), "piped_preferred.txt")
if err := os.WriteFile(path, []byte(instance), 0644); err != nil {
printWarn("Failed to write preferred Piped instance: %v", err)
}
}
func loadPreferredInstance() string {
if pipedInstanceCacheFile == "" {
return ""
}
path := filepath.Join(filepath.Dir(pipedInstanceCacheFile), "piped_preferred.txt")
data, err := os.ReadFile(path)
if err != nil {
return ""
}
return strings.TrimSpace(string(data))
}
func loadCachedPipedInstances() []string {
if pipedInstanceCacheFile == "" {
return nil
}
data, err := os.ReadFile(pipedInstanceCacheFile)
if err != nil {
return nil
}
lines := strings.Split(string(data), "\n")
var out []string
for _, l := range lines {
trimmed := strings.TrimSpace(l)
if trimmed != "" {
out = append(out, trimmed)
}
}
return out
}
func savePipedInstances(instances []string) {
if pipedInstanceCacheFile == "" {
return
}
content := strings.Join(instances, "\n")
if err := os.WriteFile(pipedInstanceCacheFile, []byte(content), 0644); err != nil {
printWarn("Failed to write piped instance list to cache: %v", err)
}
}
// VideoAPIResponse matches the structure of the JSON response from the Piped API // VideoAPIResponse matches the structure of the JSON response from the Piped API
type VideoAPIResponse struct { type VideoAPIResponse struct {
Items []struct { Items []struct {
@ -174,6 +249,7 @@ func makeHTMLRequest(query, safe, lang string, page int) (*VideoAPIResponse, err
// Store new preferred instance // Store new preferred instance
preferredInstance = inst preferredInstance = inst
savePreferredInstance(inst)
return &apiResp, nil return &apiResp, nil
} }