.fix qobuz api
This commit is contained in:
+26
-5
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -72,21 +73,41 @@ func NewQobuzDownloader() *QobuzDownloader {
|
|||||||
client: &http.Client{
|
client: &http.Client{
|
||||||
Timeout: 60 * time.Second,
|
Timeout: 60 * time.Second,
|
||||||
},
|
},
|
||||||
appID: "798273057",
|
appID: qobuzDefaultAPIAppID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *QobuzDownloader) searchByISRC(isrc string) (*QobuzTrack, error) {
|
func (q *QobuzDownloader) searchByISRC(isrc string) (*QobuzTrack, error) {
|
||||||
apiBase := "https://www.qobuz.com/api.json/0.2/track/search?query="
|
if strings.HasPrefix(isrc, "qobuz_") {
|
||||||
url := fmt.Sprintf("%s%s&limit=1&app_id=%s", apiBase, isrc, q.appID)
|
trackID := strings.TrimPrefix(isrc, "qobuz_")
|
||||||
|
resp, err := doQobuzSignedRequest(http.MethodGet, "track/get", url.Values{"track_id": {trackID}}, q.client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch track: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
resp, err := q.client.Get(url)
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("API returned status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var trackResp QobuzTrack
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&trackResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &trackResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := doQobuzSignedRequest(http.MethodGet, "track/search", url.Values{
|
||||||
|
"query": {isrc},
|
||||||
|
"limit": {"1"},
|
||||||
|
}, q.client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to search track: %w", err)
|
return nil, fmt.Errorf("failed to search track: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, fmt.Errorf("API returned status %d", resp.StatusCode)
|
return nil, fmt.Errorf("API returned status %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,407 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
qobuzAPIBaseURL = "https://www.qobuz.com/api.json/0.2"
|
||||||
|
qobuzDefaultAPIAppID = "712109809"
|
||||||
|
qobuzDefaultAPIAppSecret = "589be88e4538daea11f509d29e4a23b1"
|
||||||
|
qobuzDefaultUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
|
||||||
|
qobuzCredentialsCacheFile = "qobuz-api-credentials.json"
|
||||||
|
qobuzCredentialsCacheTTL = 24 * time.Hour
|
||||||
|
qobuzCredentialsProbeTrackISRC = "USUM71703861"
|
||||||
|
qobuzOpenTrackProbeURL = "https://open.qobuz.com/track/1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
qobuzCredentialsMu sync.Mutex
|
||||||
|
qobuzCachedCredentials *qobuzAPICredentials
|
||||||
|
qobuzOpenBundleScriptPattern = regexp.MustCompile(`<script[^>]+src="([^"]+/js/main\.js|/resources/[^"]+/js/main\.js)"`)
|
||||||
|
qobuzOpenAPIConfigPattern = regexp.MustCompile(`app_id:"(?P<app_id>\d{9})",app_secret:"(?P<app_secret>[a-f0-9]{32})"`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type qobuzAPICredentials struct {
|
||||||
|
AppID string `json:"app_id"`
|
||||||
|
AppSecret string `json:"app_secret"`
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
|
FetchedAtUnix int64 `json:"fetched_at_unix"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type qobuzCredentialProbeResponse struct {
|
||||||
|
Tracks struct {
|
||||||
|
Total int `json:"total"`
|
||||||
|
} `json:"tracks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultQobuzAPICredentials() *qobuzAPICredentials {
|
||||||
|
return &qobuzAPICredentials{
|
||||||
|
AppID: qobuzDefaultAPIAppID,
|
||||||
|
AppSecret: qobuzDefaultAPIAppSecret,
|
||||||
|
Source: "embedded-default",
|
||||||
|
FetchedAtUnix: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func qobuzCredentialsCachePath() (string, error) {
|
||||||
|
appDir, err := GetFFmpegDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Join(appDir, qobuzCredentialsCacheFile), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadQobuzCachedCredentials() (*qobuzAPICredentials, error) {
|
||||||
|
cachePath, err := qobuzCredentialsCachePath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := os.ReadFile(cachePath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to read qobuz credentials cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var creds qobuzAPICredentials
|
||||||
|
if err := json.Unmarshal(body, &creds); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse qobuz credentials cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(creds.AppID) == "" || strings.TrimSpace(creds.AppSecret) == "" {
|
||||||
|
return nil, fmt.Errorf("qobuz credentials cache is incomplete")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &creds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveQobuzCachedCredentials(creds *qobuzAPICredentials) error {
|
||||||
|
if creds == nil {
|
||||||
|
return fmt.Errorf("qobuz credentials are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
cachePath, err := qobuzCredentialsCachePath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(cachePath), 0o755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create qobuz credentials cache directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := json.MarshalIndent(creds, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(cachePath, body, 0o644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write qobuz credentials cache: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func qobuzCredentialsCacheIsFresh(creds *qobuzAPICredentials) bool {
|
||||||
|
if creds == nil || creds.FetchedAtUnix == 0 || strings.TrimSpace(creds.AppID) == "" || strings.TrimSpace(creds.AppSecret) == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return time.Since(time.Unix(creds.FetchedAtUnix, 0)) < qobuzCredentialsCacheTTL
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrapeQobuzOpenCredentials(client *http.Client) (*qobuzAPICredentials, error) {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, qobuzOpenTrackProbeURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", qobuzDefaultUA)
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch open.qobuz.com shell: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
preview, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
|
||||||
|
return nil, fmt.Errorf("open.qobuz.com returned status %d: %s", resp.StatusCode, strings.TrimSpace(string(preview)))
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read open.qobuz.com shell: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptMatch := qobuzOpenBundleScriptPattern.FindStringSubmatch(string(htmlBody))
|
||||||
|
if len(scriptMatch) < 2 {
|
||||||
|
return nil, fmt.Errorf("qobuz open bundle URL not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
bundleURL := strings.TrimSpace(scriptMatch[1])
|
||||||
|
if strings.HasPrefix(bundleURL, "/") {
|
||||||
|
bundleURL = "https://open.qobuz.com" + bundleURL
|
||||||
|
}
|
||||||
|
if bundleURL == "" {
|
||||||
|
return nil, fmt.Errorf("qobuz open bundle URL is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
bundleReq, err := http.NewRequest(http.MethodGet, bundleURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bundleReq.Header.Set("User-Agent", qobuzDefaultUA)
|
||||||
|
|
||||||
|
bundleResp, err := client.Do(bundleReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch qobuz open bundle: %w", err)
|
||||||
|
}
|
||||||
|
defer bundleResp.Body.Close()
|
||||||
|
|
||||||
|
if bundleResp.StatusCode != http.StatusOK {
|
||||||
|
preview, _ := io.ReadAll(io.LimitReader(bundleResp.Body, 512))
|
||||||
|
return nil, fmt.Errorf("qobuz open bundle returned status %d: %s", bundleResp.StatusCode, strings.TrimSpace(string(preview)))
|
||||||
|
}
|
||||||
|
|
||||||
|
bundleBody, err := io.ReadAll(bundleResp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read qobuz open bundle: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configMatch := qobuzOpenAPIConfigPattern.FindStringSubmatch(string(bundleBody))
|
||||||
|
if len(configMatch) < 3 {
|
||||||
|
return nil, fmt.Errorf("qobuz api app_id/app_secret pair not found in open bundle")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &qobuzAPICredentials{
|
||||||
|
AppID: strings.TrimSpace(configMatch[1]),
|
||||||
|
AppSecret: strings.TrimSpace(configMatch[2]),
|
||||||
|
Source: bundleURL,
|
||||||
|
FetchedAtUnix: time.Now().Unix(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func qobuzNormalizedPath(path string) string {
|
||||||
|
return strings.Trim(strings.TrimSpace(path), "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func qobuzSignaturePayload(path string, params url.Values, timestamp string, secret string) string {
|
||||||
|
normalizedPath := strings.ReplaceAll(qobuzNormalizedPath(path), "/", "")
|
||||||
|
keys := make([]string, 0, len(params))
|
||||||
|
for key := range params {
|
||||||
|
switch key {
|
||||||
|
case "app_id", "request_ts", "request_sig":
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString(normalizedPath)
|
||||||
|
for _, key := range keys {
|
||||||
|
values := params[key]
|
||||||
|
if len(values) == 0 {
|
||||||
|
builder.WriteString(key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, value := range values {
|
||||||
|
builder.WriteString(key)
|
||||||
|
builder.WriteString(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.WriteString(timestamp)
|
||||||
|
builder.WriteString(secret)
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func qobuzRequestSignature(path string, params url.Values, timestamp string, secret string) string {
|
||||||
|
sum := md5.Sum([]byte(qobuzSignaturePayload(path, params, timestamp, secret)))
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func newQobuzSignedRequestWithCredentials(method string, path string, params url.Values, creds *qobuzAPICredentials) (*http.Request, error) {
|
||||||
|
normalizedPath := qobuzNormalizedPath(path)
|
||||||
|
if normalizedPath == "" {
|
||||||
|
return nil, fmt.Errorf("qobuz request path is empty")
|
||||||
|
}
|
||||||
|
if creds == nil || strings.TrimSpace(creds.AppID) == "" || strings.TrimSpace(creds.AppSecret) == "" {
|
||||||
|
return nil, fmt.Errorf("qobuz credentials are incomplete")
|
||||||
|
}
|
||||||
|
|
||||||
|
clonedParams := url.Values{}
|
||||||
|
for key, values := range params {
|
||||||
|
for _, value := range values {
|
||||||
|
clonedParams.Add(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp := fmt.Sprintf("%d", time.Now().Unix())
|
||||||
|
clonedParams.Set("app_id", creds.AppID)
|
||||||
|
clonedParams.Set("request_ts", timestamp)
|
||||||
|
clonedParams.Set("request_sig", qobuzRequestSignature(normalizedPath, params, timestamp, creds.AppSecret))
|
||||||
|
|
||||||
|
reqURL := fmt.Sprintf("%s/%s?%s", qobuzAPIBaseURL, normalizedPath, clonedParams.Encode())
|
||||||
|
req, err := http.NewRequest(method, reqURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("User-Agent", qobuzDefaultUA)
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("X-App-Id", creds.AppID)
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func qobuzCredentialsSupportSignedMetadata(client *http.Client, creds *qobuzAPICredentials) bool {
|
||||||
|
if creds == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := newQobuzSignedRequestWithCredentials(http.MethodGet, "track/search", url.Values{
|
||||||
|
"query": {qobuzCredentialsProbeTrackISRC},
|
||||||
|
"limit": {"1"},
|
||||||
|
}, creds)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload qobuzCredentialProbeResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload.Tracks.Total > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func getQobuzAPICredentials(forceRefresh bool) (*qobuzAPICredentials, error) {
|
||||||
|
qobuzCredentialsMu.Lock()
|
||||||
|
defer qobuzCredentialsMu.Unlock()
|
||||||
|
|
||||||
|
if !forceRefresh && qobuzCredentialsCacheIsFresh(qobuzCachedCredentials) {
|
||||||
|
return qobuzCachedCredentials, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedFromDisk, diskErr := loadQobuzCachedCredentials()
|
||||||
|
if diskErr != nil {
|
||||||
|
fmt.Printf("Warning: failed to read Qobuz credentials cache: %v\n", diskErr)
|
||||||
|
}
|
||||||
|
if !forceRefresh && qobuzCredentialsCacheIsFresh(cachedFromDisk) {
|
||||||
|
qobuzCachedCredentials = cachedFromDisk
|
||||||
|
return qobuzCachedCredentials, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
|
scrapedCreds, scrapeErr := scrapeQobuzOpenCredentials(client)
|
||||||
|
if scrapeErr == nil {
|
||||||
|
if qobuzCredentialsSupportSignedMetadata(client, scrapedCreds) {
|
||||||
|
qobuzCachedCredentials = scrapedCreds
|
||||||
|
if err := saveQobuzCachedCredentials(scrapedCreds); err != nil {
|
||||||
|
fmt.Printf("Warning: failed to write Qobuz credentials cache: %v\n", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Loaded fresh Qobuz credentials from %s (app_id=%s)\n", scrapedCreds.Source, scrapedCreds.AppID)
|
||||||
|
return qobuzCachedCredentials, nil
|
||||||
|
}
|
||||||
|
scrapeErr = fmt.Errorf("scraped qobuz credentials did not pass validation")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cachedFromDisk != nil {
|
||||||
|
qobuzCachedCredentials = cachedFromDisk
|
||||||
|
fmt.Printf("Warning: failed to refresh Qobuz credentials, using cached credentials: %v\n", scrapeErr)
|
||||||
|
return qobuzCachedCredentials, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if qobuzCachedCredentials != nil {
|
||||||
|
fmt.Printf("Warning: failed to refresh Qobuz credentials, using in-memory credentials: %v\n", scrapeErr)
|
||||||
|
return qobuzCachedCredentials, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fallback := defaultQobuzAPICredentials()
|
||||||
|
qobuzCachedCredentials = fallback
|
||||||
|
if scrapeErr != nil {
|
||||||
|
fmt.Printf("Warning: failed to refresh Qobuz credentials, using embedded fallback: %v\n", scrapeErr)
|
||||||
|
}
|
||||||
|
return qobuzCachedCredentials, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func qobuzShouldRefreshCredentials(statusCode int) bool {
|
||||||
|
return statusCode == http.StatusBadRequest || statusCode == http.StatusUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
func newQobuzSignedRequest(method string, path string, params url.Values) (*http.Request, error) {
|
||||||
|
creds, err := getQobuzAPICredentials(false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newQobuzSignedRequestWithCredentials(method, path, params, creds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func doQobuzSignedRequest(method string, path string, params url.Values, client *http.Client) (*http.Response, error) {
|
||||||
|
if client == nil {
|
||||||
|
client = &http.Client{Timeout: 20 * time.Second}
|
||||||
|
}
|
||||||
|
|
||||||
|
call := func(forceRefresh bool) (*http.Response, error) {
|
||||||
|
creds, err := getQobuzAPICredentials(forceRefresh)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req, err := newQobuzSignedRequestWithCredentials(method, path, params, creds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return client.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := call(false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if qobuzShouldRefreshCredentials(resp.StatusCode) {
|
||||||
|
resp.Body.Close()
|
||||||
|
return call(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func doQobuzSignedJSONRequest(path string, params url.Values, target interface{}) error {
|
||||||
|
resp, err := doQobuzSignedRequest(http.MethodGet, path, params, &http.Client{Timeout: 20 * time.Second})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
snippet, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
||||||
|
return fmt.Errorf("qobuz request failed: HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(snippet)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.NewDecoder(resp.Body).Decode(target)
|
||||||
|
}
|
||||||
+5
-20
@@ -129,31 +129,16 @@ func (s *SongLinkClient) CheckTrackAvailability(spotifyTrackID string) (*TrackAv
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkQobuzAvailability(isrc string) bool {
|
func checkQobuzAvailability(isrc string) bool {
|
||||||
client := &http.Client{Timeout: 10 * time.Second}
|
|
||||||
appID := "798273057"
|
|
||||||
|
|
||||||
searchURL := fmt.Sprintf(
|
|
||||||
"https://www.qobuz.com/api.json/0.2/track/search?query=%s&limit=1&app_id=%s",
|
|
||||||
url.QueryEscape(strings.TrimSpace(isrc)),
|
|
||||||
appID,
|
|
||||||
)
|
|
||||||
|
|
||||||
resp, err := client.Get(searchURL)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var searchResp struct {
|
var searchResp struct {
|
||||||
Tracks struct {
|
Tracks struct {
|
||||||
Total int `json:"total"`
|
Total int `json:"total"`
|
||||||
} `json:"tracks"`
|
} `json:"tracks"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil {
|
|
||||||
|
if err := doQobuzSignedJSONRequest("track/search", url.Values{
|
||||||
|
"query": {strings.TrimSpace(isrc)},
|
||||||
|
"limit": {"1"},
|
||||||
|
}, &searchResp); err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user