.priority api
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
+10
-7
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user