.priority api

This commit is contained in:
afkarxyz
2026-04-02 08:29:37 +07:00
parent b96fc8d96c
commit 7ce66b4732
4 changed files with 235 additions and 14 deletions
+4
View File
@@ -49,11 +49,15 @@ func (a *App) startup(ctx context.Context) {
if err := backend.InitISRCCacheDB(); err != nil { if err := backend.InitISRCCacheDB(); err != nil {
fmt.Printf("Failed to init ISRC cache DB: %v\n", err) 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) { func (a *App) shutdown(ctx context.Context) {
backend.CloseHistoryDB() backend.CloseHistoryDB()
backend.CloseISRCCacheDB() backend.CloseISRCCacheDB()
backend.CloseProviderPriorityDB()
} }
type SpotifyMetadataRequest struct { type SpotifyMetadataRequest struct {
+215
View File
@@ -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)
}
+6 -7
View File
@@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"math/rand"
"net/http" "net/http"
"os" "os"
"path/filepath" "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) 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://dab.yeet.su/api/stream?trackId=",
"https://dabmusic.xyz/api/stream?trackId=", "https://dabmusic.xyz/api/stream?trackId=",
"https://qbz.afkarxyz.qzz.io/api/track/", "https://qbz.afkarxyz.qzz.io/api/track/",
} })
downloadFunc := func(qual string) (string, error) { downloadFunc := func(qual string) (string, error) {
type Provider struct { type Provider struct {
Name string Name string
API string
Func func() (string, error) Func func() (string, error)
} }
@@ -189,27 +189,26 @@ func (q *QobuzDownloader) GetDownloadURL(trackID int64, quality string, allowFal
currentAPI := api currentAPI := api
providers = append(providers, Provider{ providers = append(providers, Provider{
Name: "Standard(" + currentAPI + ")", Name: "Standard(" + currentAPI + ")",
API: currentAPI,
Func: func() (string, error) { Func: func() (string, error) {
return q.DownloadFromStandard(currentAPI, trackID, qual) 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 var lastErr error
for _, p := range providers { for _, p := range providers {
fmt.Printf("Trying Provider: %s (Quality: %s)...\n", p.Name, qual) fmt.Printf("Trying Provider: %s (Quality: %s)...\n", p.Name, qual)
url, err := p.Func() url, err := p.Func()
if err == nil { if err == nil {
fmt.Printf("✓ Success\n") fmt.Printf("✓ Success\n")
recordProviderSuccess("qobuz", p.API)
return url, nil return url, nil
} }
fmt.Printf("Provider failed: %v\n", err) fmt.Printf("Provider failed: %v\n", err)
recordProviderFailure("qobuz", p.API)
lastErr = err lastErr = err
} }
return "", lastErr return "", lastErr
+10 -7
View File
@@ -6,7 +6,6 @@ import (
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"io" "io"
"math/rand"
"net/http" "net/http"
"os" "os"
"os/exec" "os/exec"
@@ -86,7 +85,7 @@ func (t *TidalDownloader) GetAvailableAPIs() ([]string, error) {
"https://monochrome-api.samidy.com", "https://monochrome-api.samidy.com",
"https://tidal.kinoplus.online", "https://tidal.kinoplus.online",
} }
return apis, nil return prioritizeProviders("tidal", apis), nil
} }
func (t *TidalDownloader) GetTidalURLFromSpotify(spotifyTrackID string) (string, error) { 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") return "", "", fmt.Errorf("no APIs available")
} }
rand.Seed(time.Now().UnixNano()) orderedAPIs := prioritizeProviders("tidal", apis)
rand.Shuffle(len(apis), func(i, j int) { apis[i], apis[j] = apis[j], apis[i] }) fmt.Printf("Trying %d prioritized APIs...\n", len(orderedAPIs))
fmt.Printf("Rotating through %d APIs...\n", len(apis))
var lastError error var lastError error
var errors []string var errors []string
for _, apiURL := range apis { for _, apiURL := range orderedAPIs {
fmt.Printf("Trying API: %s\n", apiURL) fmt.Printf("Trying API: %s\n", apiURL)
client := &http.Client{ client := &http.Client{
@@ -925,6 +922,7 @@ func getDownloadURLRotated(apis []string, trackID int64, quality string) (string
resp, err := client.Get(url) resp, err := client.Get(url)
if err != nil { if err != nil {
lastError = err lastError = err
recordProviderFailure("tidal", apiURL)
errors = append(errors, fmt.Sprintf("%s: %v", apiURL, err)) errors = append(errors, fmt.Sprintf("%s: %v", apiURL, err))
continue continue
} }
@@ -932,6 +930,7 @@ func getDownloadURLRotated(apis []string, trackID int64, quality string) (string
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
resp.Body.Close() resp.Body.Close()
lastError = fmt.Errorf("HTTP %d", resp.StatusCode) lastError = fmt.Errorf("HTTP %d", resp.StatusCode)
recordProviderFailure("tidal", apiURL)
errors = append(errors, fmt.Sprintf("%s: %v", apiURL, lastError)) errors = append(errors, fmt.Sprintf("%s: %v", apiURL, lastError))
continue continue
} }
@@ -940,6 +939,7 @@ func getDownloadURLRotated(apis []string, trackID int64, quality string) (string
resp.Body.Close() resp.Body.Close()
if err != nil { if err != nil {
lastError = err lastError = err
recordProviderFailure("tidal", apiURL)
errors = append(errors, fmt.Sprintf("%s: read body failed", apiURL)) errors = append(errors, fmt.Sprintf("%s: read body failed", apiURL))
continue continue
} }
@@ -947,6 +947,7 @@ func getDownloadURLRotated(apis []string, trackID int64, quality string) (string
var v2Response TidalAPIResponseV2 var v2Response TidalAPIResponseV2
if err := json.Unmarshal(body, &v2Response); err == nil && v2Response.Data.Manifest != "" { if err := json.Unmarshal(body, &v2Response); err == nil && v2Response.Data.Manifest != "" {
fmt.Printf("✓ Success with: %s\n", apiURL) fmt.Printf("✓ Success with: %s\n", apiURL)
recordProviderSuccess("tidal", apiURL)
return apiURL, "MANIFEST:" + v2Response.Data.Manifest, nil return apiURL, "MANIFEST:" + v2Response.Data.Manifest, nil
} }
@@ -955,12 +956,14 @@ func getDownloadURLRotated(apis []string, trackID int64, quality string) (string
for _, item := range v1Responses { for _, item := range v1Responses {
if item.OriginalTrackURL != "" { if item.OriginalTrackURL != "" {
fmt.Printf("✓ Success with: %s\n", apiURL) fmt.Printf("✓ Success with: %s\n", apiURL)
recordProviderSuccess("tidal", apiURL)
return apiURL, item.OriginalTrackURL, nil return apiURL, item.OriginalTrackURL, nil
} }
} }
} }
lastError = fmt.Errorf("no download URL or manifest in response") lastError = fmt.Errorf("no download URL or manifest in response")
recordProviderFailure("tidal", apiURL)
errors = append(errors, fmt.Sprintf("%s: %v", apiURL, lastError)) errors = append(errors, fmt.Sprintf("%s: %v", apiURL, lastError))
} }