From be973266c630cb70c47a037fe7d8d36c49e6e200 Mon Sep 17 00:00:00 2001 From: partisan Date: Tue, 10 Jun 2025 21:28:40 +0200 Subject: [PATCH] Cache preferred Piped instance to furde reduce start delay --- music-youtube.go | 50 +++++++++++++++++++++------- video.go | 86 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 120 insertions(+), 16 deletions(-) diff --git a/music-youtube.go b/music-youtube.go index 698dc71..d262428 100644 --- a/music-youtube.go +++ b/music-youtube.go @@ -19,14 +19,13 @@ type MusicAPIResponse struct { func SearchMusicViaPiped(query string, page int) ([]MusicResult, error) { var lastError error + + // We will try to use preferred instance mu.Lock() - defer mu.Unlock() - - for _, instance := range pipedInstances { - if disabledInstances[instance] { - continue - } + instance := preferredInstance + mu.Unlock() + if instance != "" && !disabledInstances[instance] { url := fmt.Sprintf( "https://%s/search?q=%s&filter=music_songs&page=%d", instance, @@ -34,22 +33,51 @@ func SearchMusicViaPiped(query string, page int) ([]MusicResult, error) { 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) if err != nil || resp.StatusCode != http.StatusOK { - printInfo("Disabling instance %s due to error: %v", instance, err) - disabledInstances[instance] = true - lastError = fmt.Errorf("request to %s failed: %w", instance, err) + printInfo("Disabling instance %s due to error: %v", inst, err) + disabledInstances[inst] = true + lastError = fmt.Errorf("request to %s failed: %w", inst, err) continue } defer resp.Body.Close() var apiResp MusicAPIResponse 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 } - return convertPipedToMusicResults(instance, apiResp), nil + preferredInstance = inst + return convertPipedToMusicResults(inst, apiResp), nil } return nil, fmt.Errorf("all Piped instances failed, last error: %v", lastError) diff --git a/video.go b/video.go index 2c8abe4..8e3ebd0 100644 --- a/video.go +++ b/video.go @@ -5,6 +5,9 @@ import ( "fmt" "net/http" "net/url" + "os" + "path/filepath" + "strings" "sync" "time" ) @@ -12,18 +15,90 @@ import ( const retryDuration = 12 * time.Hour // Retry duration for unresponding piped instances var ( - preferredInstance string - pipedInstances = []string{} - disabledInstances = make(map[string]bool) - mu sync.Mutex + preferredInstance string + pipedInstanceCacheFile string + pipedInstances = []string{} + disabledInstances = make(map[string]bool) + mu sync.Mutex ) 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 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 type VideoAPIResponse struct { Items []struct { @@ -174,6 +249,7 @@ func makeHTMLRequest(query, safe, lang string, page int) (*VideoAPIResponse, err // Store new preferred instance preferredInstance = inst + savePreferredInstance(inst) return &apiResp, nil }