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