From 7ce66b473291157faf87155f8dd8457ab37d2bd0 Mon Sep 17 00:00:00 2001 From: afkarxyz Date: Thu, 2 Apr 2026 08:29:37 +0700 Subject: [PATCH] .priority api --- app.go | 4 + backend/provider_priority.go | 215 +++++++++++++++++++++++++++++++++++ backend/qobuz.go | 13 +-- backend/tidal.go | 17 +-- 4 files changed, 235 insertions(+), 14 deletions(-) create mode 100644 backend/provider_priority.go diff --git a/app.go b/app.go index 8479c5a..179548d 100644 --- a/app.go +++ b/app.go @@ -49,11 +49,15 @@ func (a *App) startup(ctx context.Context) { if err := backend.InitISRCCacheDB(); err != nil { fmt.Printf("Failed to init ISRC cache DB: %v\n", err) } + if err := backend.InitProviderPriorityDB(); err != nil { + fmt.Printf("Failed to init provider priority DB: %v\n", err) + } } func (a *App) shutdown(ctx context.Context) { backend.CloseHistoryDB() backend.CloseISRCCacheDB() + backend.CloseProviderPriorityDB() } type SpotifyMetadataRequest struct { diff --git a/backend/provider_priority.go b/backend/provider_priority.go new file mode 100644 index 0000000..68377ef --- /dev/null +++ b/backend/provider_priority.go @@ -0,0 +1,215 @@ +package backend + +import ( + "encoding/json" + "fmt" + "path/filepath" + "sort" + "strings" + "sync" + "time" + + bolt "go.etcd.io/bbolt" +) + +const ( + providerPriorityDBFile = "provider_priority.db" + providerPriorityBucket = "ProviderPriority" +) + +type providerPriorityEntry struct { + Service string `json:"service"` + Provider string `json:"provider"` + LastOutcome string `json:"last_outcome"` + LastAttempt int64 `json:"last_attempt"` + LastSuccess int64 `json:"last_success"` + LastFailure int64 `json:"last_failure"` + SuccessCount int64 `json:"success_count"` + FailureCount int64 `json:"failure_count"` +} + +var ( + providerPriorityDB *bolt.DB + providerPriorityDBMu sync.Mutex +) + +func InitProviderPriorityDB() error { + providerPriorityDBMu.Lock() + defer providerPriorityDBMu.Unlock() + + if providerPriorityDB != nil { + return nil + } + + appDir, err := EnsureAppDir() + if err != nil { + return err + } + + dbPath := filepath.Join(appDir, providerPriorityDBFile) + db, err := bolt.Open(dbPath, 0o600, &bolt.Options{Timeout: 1 * time.Second}) + if err != nil { + return err + } + + if err := db.Update(func(tx *bolt.Tx) error { + _, err := tx.CreateBucketIfNotExists([]byte(providerPriorityBucket)) + return err + }); err != nil { + db.Close() + return err + } + + providerPriorityDB = db + return nil +} + +func CloseProviderPriorityDB() { + providerPriorityDBMu.Lock() + defer providerPriorityDBMu.Unlock() + + if providerPriorityDB != nil { + _ = providerPriorityDB.Close() + providerPriorityDB = nil + } +} + +func prioritizeProviders(service string, providers []string) []string { + ordered := append([]string(nil), providers...) + if len(ordered) < 2 { + return ordered + } + + if err := InitProviderPriorityDB(); err != nil { + fmt.Printf("Warning: failed to init provider priority DB: %v\n", err) + return ordered + } + + serviceKey := strings.TrimSpace(strings.ToLower(service)) + entries := make(map[string]providerPriorityEntry, len(ordered)) + + if err := providerPriorityDB.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(providerPriorityBucket)) + if bucket == nil { + return nil + } + + for _, provider := range ordered { + if raw := bucket.Get([]byte(providerPriorityKey(serviceKey, provider))); len(raw) > 0 { + var entry providerPriorityEntry + if err := json.Unmarshal(raw, &entry); err != nil { + return err + } + entries[provider] = entry + } + } + return nil + }); err != nil { + fmt.Printf("Warning: failed to read provider priority DB: %v\n", err) + return ordered + } + + originalIndex := make(map[string]int, len(ordered)) + for idx, provider := range ordered { + originalIndex[provider] = idx + } + + sort.SliceStable(ordered, func(i, j int) bool { + left := entries[ordered[i]] + right := entries[ordered[j]] + + leftRank := providerOutcomeRank(left.LastOutcome) + rightRank := providerOutcomeRank(right.LastOutcome) + if leftRank != rightRank { + return leftRank > rightRank + } + + if left.LastSuccess != right.LastSuccess { + return left.LastSuccess > right.LastSuccess + } + + if left.LastAttempt != right.LastAttempt { + return left.LastAttempt > right.LastAttempt + } + + return originalIndex[ordered[i]] < originalIndex[ordered[j]] + }) + + return ordered +} + +func recordProviderSuccess(service string, provider string) { + recordProviderOutcome(service, provider, true) +} + +func recordProviderFailure(service string, provider string) { + recordProviderOutcome(service, provider, false) +} + +func recordProviderOutcome(service string, provider string, success bool) { + if strings.TrimSpace(service) == "" || strings.TrimSpace(provider) == "" { + return + } + + if err := InitProviderPriorityDB(); err != nil { + fmt.Printf("Warning: failed to init provider priority DB: %v\n", err) + return + } + + serviceKey := strings.TrimSpace(strings.ToLower(service)) + providerKey := providerPriorityKey(serviceKey, provider) + now := time.Now().Unix() + + if err := providerPriorityDB.Update(func(tx *bolt.Tx) error { + bucket, err := tx.CreateBucketIfNotExists([]byte(providerPriorityBucket)) + if err != nil { + return err + } + + entry := providerPriorityEntry{ + Service: serviceKey, + Provider: provider, + } + + if raw := bucket.Get([]byte(providerKey)); len(raw) > 0 { + if err := json.Unmarshal(raw, &entry); err != nil { + return err + } + } + + entry.LastAttempt = now + if success { + entry.LastOutcome = "success" + entry.LastSuccess = now + entry.SuccessCount++ + } else { + entry.LastOutcome = "failure" + entry.LastFailure = now + entry.FailureCount++ + } + + payload, err := json.Marshal(entry) + if err != nil { + return err + } + + return bucket.Put([]byte(providerKey), payload) + }); err != nil { + fmt.Printf("Warning: failed to update provider priority DB: %v\n", err) + } +} + +func providerOutcomeRank(outcome string) int { + switch strings.TrimSpace(strings.ToLower(outcome)) { + case "success": + return 2 + case "": + return 1 + default: + return 0 + } +} + +func providerPriorityKey(service string, provider string) string { + return strings.TrimSpace(strings.ToLower(service)) + "|" + strings.TrimSpace(provider) +} diff --git a/backend/qobuz.go b/backend/qobuz.go index c77e0b2..bf9b349 100644 --- a/backend/qobuz.go +++ b/backend/qobuz.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "io" - "math/rand" "net/http" "os" "path/filepath" @@ -171,15 +170,16 @@ func (q *QobuzDownloader) GetDownloadURL(trackID int64, quality string, allowFal fmt.Printf("Getting download URL for track ID: %d with requested quality: %s\n", trackID, qualityCode) - standardAPIs := []string{ + standardAPIs := prioritizeProviders("qobuz", []string{ "https://dab.yeet.su/api/stream?trackId=", "https://dabmusic.xyz/api/stream?trackId=", "https://qbz.afkarxyz.qzz.io/api/track/", - } + }) downloadFunc := func(qual string) (string, error) { type Provider struct { Name string + API string Func func() (string, error) } @@ -189,27 +189,26 @@ func (q *QobuzDownloader) GetDownloadURL(trackID int64, quality string, allowFal currentAPI := api providers = append(providers, Provider{ Name: "Standard(" + currentAPI + ")", + API: currentAPI, Func: func() (string, error) { return q.DownloadFromStandard(currentAPI, trackID, qual) }, }) } - rand.Seed(time.Now().UnixNano()) - rand.Shuffle(len(providers), func(i, j int) { providers[i], providers[j] = providers[j], providers[i] }) - var lastErr error for _, p := range providers { - fmt.Printf("Trying Provider: %s (Quality: %s)...\n", p.Name, qual) url, err := p.Func() if err == nil { fmt.Printf("✓ Success\n") + recordProviderSuccess("qobuz", p.API) return url, nil } fmt.Printf("Provider failed: %v\n", err) + recordProviderFailure("qobuz", p.API) lastErr = err } return "", lastErr diff --git a/backend/tidal.go b/backend/tidal.go index 630ee7b..e9234d9 100644 --- a/backend/tidal.go +++ b/backend/tidal.go @@ -6,7 +6,6 @@ import ( "encoding/xml" "fmt" "io" - "math/rand" "net/http" "os" "os/exec" @@ -86,7 +85,7 @@ func (t *TidalDownloader) GetAvailableAPIs() ([]string, error) { "https://monochrome-api.samidy.com", "https://tidal.kinoplus.online", } - return apis, nil + return prioritizeProviders("tidal", apis), nil } func (t *TidalDownloader) GetTidalURLFromSpotify(spotifyTrackID string) (string, error) { @@ -906,15 +905,13 @@ func getDownloadURLRotated(apis []string, trackID int64, quality string) (string return "", "", fmt.Errorf("no APIs available") } - rand.Seed(time.Now().UnixNano()) - rand.Shuffle(len(apis), func(i, j int) { apis[i], apis[j] = apis[j], apis[i] }) - - fmt.Printf("Rotating through %d APIs...\n", len(apis)) + orderedAPIs := prioritizeProviders("tidal", apis) + fmt.Printf("Trying %d prioritized APIs...\n", len(orderedAPIs)) var lastError error var errors []string - for _, apiURL := range apis { + for _, apiURL := range orderedAPIs { fmt.Printf("Trying API: %s\n", apiURL) client := &http.Client{ @@ -925,6 +922,7 @@ func getDownloadURLRotated(apis []string, trackID int64, quality string) (string resp, err := client.Get(url) if err != nil { lastError = err + recordProviderFailure("tidal", apiURL) errors = append(errors, fmt.Sprintf("%s: %v", apiURL, err)) continue } @@ -932,6 +930,7 @@ func getDownloadURLRotated(apis []string, trackID int64, quality string) (string if resp.StatusCode != 200 { resp.Body.Close() lastError = fmt.Errorf("HTTP %d", resp.StatusCode) + recordProviderFailure("tidal", apiURL) errors = append(errors, fmt.Sprintf("%s: %v", apiURL, lastError)) continue } @@ -940,6 +939,7 @@ func getDownloadURLRotated(apis []string, trackID int64, quality string) (string resp.Body.Close() if err != nil { lastError = err + recordProviderFailure("tidal", apiURL) errors = append(errors, fmt.Sprintf("%s: read body failed", apiURL)) continue } @@ -947,6 +947,7 @@ func getDownloadURLRotated(apis []string, trackID int64, quality string) (string var v2Response TidalAPIResponseV2 if err := json.Unmarshal(body, &v2Response); err == nil && v2Response.Data.Manifest != "" { fmt.Printf("✓ Success with: %s\n", apiURL) + recordProviderSuccess("tidal", apiURL) return apiURL, "MANIFEST:" + v2Response.Data.Manifest, nil } @@ -955,12 +956,14 @@ func getDownloadURLRotated(apis []string, trackID int64, quality string) (string for _, item := range v1Responses { if item.OriginalTrackURL != "" { fmt.Printf("✓ Success with: %s\n", apiURL) + recordProviderSuccess("tidal", apiURL) return apiURL, item.OriginalTrackURL, nil } } } lastError = fmt.Errorf("no download URL or manifest in response") + recordProviderFailure("tidal", apiURL) errors = append(errors, fmt.Sprintf("%s: %v", apiURL, lastError)) }