.tidal gist url

This commit is contained in:
afkarxyz
2026-04-19 22:15:49 +07:00
parent a3e780587b
commit 3af9327a3d
12 changed files with 655 additions and 329 deletions
+96 -103
View File
@@ -49,17 +49,9 @@ type TidalBTSManifest struct {
}
func NewTidalDownloader(apiURL string) *TidalDownloader {
apiURL = strings.TrimRight(strings.TrimSpace(apiURL), "/")
if apiURL == "" {
downloader := &TidalDownloader{
client: &http.Client{
Timeout: 5 * time.Second,
},
timeout: 5 * time.Second,
maxRetries: 3,
apiURL: "",
}
apis, err := downloader.GetAvailableAPIs()
apis, err := GetRotatedTidalAPIList()
if err == nil && len(apis) > 0 {
apiURL = apis[0]
}
@@ -76,16 +68,12 @@ func NewTidalDownloader(apiURL string) *TidalDownloader {
}
func (t *TidalDownloader) GetAvailableAPIs() ([]string, error) {
apis := []string{
"https://hifi-one.spotisaver.net",
"https://hifi-two.spotisaver.net",
"https://eu-central.monochrome.tf",
"https://us-west.monochrome.tf",
"https://api.monochrome.tf",
"https://monochrome-api.samidy.com",
"https://tidal.kinoplus.online",
apis, err := GetRotatedTidalAPIList()
if err == nil && len(apis) > 0 {
return apis, nil
}
return prioritizeProviders("tidal", apis), nil
return nil, err
}
func (t *TidalDownloader) GetTidalURLFromSpotify(spotifyTrackID string) (string, error) {
@@ -129,14 +117,12 @@ func (t *TidalDownloader) GetDownloadURL(trackID int64, quality string) (string,
url := fmt.Sprintf("%s/track/?id=%d&quality=%s", t.apiURL, trackID, quality)
fmt.Printf("Tidal API URL: %s\n", url)
req, err := http.NewRequest("GET", url, nil)
req, err := NewRequestWithDefaultHeaders(http.MethodGet, url, nil)
if err != nil {
fmt.Printf("✗ failed to create request: %v\n", err)
return "", fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36")
resp, err := t.client.Do(req)
if err != nil {
fmt.Printf("✗ Tidal API request failed: %v\n", err)
@@ -194,13 +180,11 @@ func (t *TidalDownloader) DownloadFile(url, filepath string) error {
return t.DownloadFromManifest(strings.TrimPrefix(url, "MANIFEST:"), filepath)
}
req, err := http.NewRequest("GET", url, nil)
req, err := NewRequestWithDefaultHeaders(http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36")
resp, err := t.client.Do(req)
if err != nil {
@@ -241,11 +225,10 @@ func (t *TidalDownloader) DownloadFromManifest(manifestB64, outputPath string) e
}
doRequest := func(url string) (*http.Response, error) {
req, err := http.NewRequest("GET", url, nil)
req, err := NewRequestWithDefaultHeaders(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36")
return client.Do(req)
}
@@ -460,7 +443,7 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
downloadURL, err := t.GetDownloadURL(trackID, quality)
if err != nil {
if quality == "HI_RES" && allowFallback {
if isTidalHiResQuality(quality) && allowFallback {
fmt.Println("⚠ HI_RES unavailable/failed, falling back to LOSSLESS...")
downloadURL, err = t.GetDownloadURL(trackID, "LOSSLESS")
if err != nil {
@@ -515,6 +498,11 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
if err := t.DownloadFile(downloadURL, outputFilename); err != nil {
return "", err
}
if t.apiURL != "" {
if err := RememberTidalAPIUsage(t.apiURL); err != nil {
fmt.Printf("Warning: failed to persist last used Tidal API: %v\n", err)
}
}
isrc := strings.TrimSpace(isrcOverride)
var mbMeta Metadata
@@ -580,11 +568,6 @@ func (t *TidalDownloader) DownloadByURL(tidalURL, outputDir, quality, filenameFo
}
func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality, filenameFormat string, includeTrackNumber bool, position int, spotifyTrackName, spotifyArtistName, spotifyAlbumName, spotifyAlbumArtist, spotifyReleaseDate string, useAlbumTrackNumber bool, spotifyCoverURL string, embedMaxQualityCover bool, spotifyTrackNumber, spotifyDiscNumber, spotifyTotalTracks int, spotifyTotalDiscs int, spotifyCopyright, spotifyPublisher, spotifyComposer, metadataSeparator, isrcOverride, spotifyURL string, allowFallback bool, useFirstArtistOnly bool, useSingleGenre bool, embedGenre bool) (string, error) {
apis, err := t.GetAvailableAPIs()
if err != nil {
return "", fmt.Errorf("no APIs available for fallback: %w", err)
}
if outputDir != "." {
if err := os.MkdirAll(outputDir, 0755); err != nil {
return "", fmt.Errorf("directory error: %w", err)
@@ -626,19 +609,6 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
return "EXISTS:" + outputFilename, nil
}
successAPI, downloadURL, err := getDownloadURLRotated(apis, trackID, quality)
if err != nil {
if quality == "HI_RES" && allowFallback {
fmt.Println("⚠ HI_RES unavailable/failed on all APIs, falling back to LOSSLESS...")
successAPI, downloadURL, err = getDownloadURLRotated(apis, trackID, "LOSSLESS")
if err != nil {
return "", fmt.Errorf("failed to get download URL (HI_RES & LOSSLESS both failed): %w", err)
}
} else {
return "", err
}
}
type mbResultFallback struct {
ISRC string
Metadata Metadata
@@ -680,10 +650,11 @@ func (t *TidalDownloader) DownloadByURLWithFallback(tidalURL, outputDir, quality
}
fmt.Printf("Downloading to: %s\n", outputFilename)
downloader := NewTidalDownloader(successAPI)
if err := downloader.DownloadFile(downloadURL, outputFilename); err != nil {
successAPI, err := t.downloadWithRotatingAPIs(trackID, outputFilename, quality, allowFallback)
if err != nil {
return "", err
}
fmt.Printf("✓ Downloaded using API: %s\n", successAPI)
isrc := strings.TrimSpace(isrcOverride)
var mbMeta Metadata
@@ -920,79 +891,101 @@ func parseManifest(manifestB64 string) (directURL string, initURL string, mediaU
return "", initURL, mediaURLs, "", nil
}
func getDownloadURLRotated(apis []string, trackID int64, quality string) (string, string, error) {
if len(apis) == 0 {
return "", "", fmt.Errorf("no APIs available")
func (t *TidalDownloader) downloadWithRotatingAPIs(trackID int64, outputFilename string, quality string, allowFallback bool) (string, error) {
qualities := []string{quality}
if isTidalHiResQuality(quality) && allowFallback {
qualities = append(qualities, "LOSSLESS")
}
orderedAPIs := prioritizeProviders("tidal", apis)
fmt.Printf("Trying %d prioritized APIs...\n", len(orderedAPIs))
var lastError error
var errors []string
for _, apiURL := range orderedAPIs {
fmt.Printf("Trying API: %s\n", apiURL)
client := &http.Client{
Timeout: 15 * time.Second,
var lastErr error
for idx, candidateQuality := range qualities {
if idx > 0 {
fmt.Printf("⚠ %s unavailable/failed on all APIs, falling back to %s...\n", quality, candidateQuality)
}
url := fmt.Sprintf("%s/track/?id=%d&quality=%s", apiURL, trackID, quality)
resp, err := client.Get(url)
apiURL, err := t.tryDownloadAcrossTidalAPIs(trackID, outputFilename, candidateQuality, false)
if err == nil {
return apiURL, nil
}
lastErr = err
}
if lastErr == nil {
lastErr = fmt.Errorf("no tidal api succeeded")
}
return "", lastErr
}
func (t *TidalDownloader) tryDownloadAcrossTidalAPIs(trackID int64, outputFilename string, quality string, refreshed bool) (string, error) {
apis, err := GetRotatedTidalAPIList()
if err != nil && len(apis) == 0 {
return "", fmt.Errorf("failed to load tidal api list: %w", err)
}
if len(apis) == 0 {
return "", fmt.Errorf("no tidal apis available")
}
var lastErr error
errors := make([]string, 0, len(apis))
for _, apiURL := range apis {
fmt.Printf("Trying Tidal API: %s\n", apiURL)
downloader := NewTidalDownloader(apiURL)
downloadURL, err := downloader.GetDownloadURL(trackID, quality)
if err != nil {
lastError = err
recordProviderFailure("tidal", apiURL)
lastErr = err
errors = append(errors, fmt.Sprintf("%s: %v", apiURL, err))
continue
}
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))
if err := downloader.DownloadFile(downloadURL, outputFilename); err != nil {
lastErr = err
cleanupTidalDownloadArtifacts(outputFilename)
errors = append(errors, fmt.Sprintf("%s: %v", apiURL, err))
continue
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
lastError = err
recordProviderFailure("tidal", apiURL)
errors = append(errors, fmt.Sprintf("%s: read body failed", apiURL))
continue
if err := RememberTidalAPIUsage(apiURL); err != nil {
fmt.Printf("Warning: failed to persist last used Tidal API: %v\n", err)
}
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
}
var v1Responses []TidalAPIResponse
if err := json.Unmarshal(body, &v1Responses); err == nil {
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))
return apiURL, nil
}
fmt.Println("All APIs failed:")
for _, e := range errors {
fmt.Printf(" ✗ %s\n", e)
if !refreshed {
if _, refreshErr := RefreshTidalAPIList(true); refreshErr != nil {
errors = append(errors, fmt.Sprintf("gist refresh failed: %v", refreshErr))
} else {
fmt.Println("All cached Tidal APIs failed, refreshed gist list and retrying...")
return t.tryDownloadAcrossTidalAPIs(trackID, outputFilename, quality, true)
}
}
return "", "", fmt.Errorf("all %d APIs failed. Last error: %v", len(apis), lastError)
if lastErr == nil {
lastErr = fmt.Errorf("all tidal apis failed")
}
fmt.Println("All Tidal APIs failed:")
for _, item := range errors {
fmt.Printf(" ✗ %s\n", item)
}
return "", fmt.Errorf("all tidal apis failed for quality %s: %w", quality, lastErr)
}
func cleanupTidalDownloadArtifacts(outputPath string) {
if outputPath == "" {
return
}
_ = os.Remove(outputPath)
_ = os.Remove(outputPath + ".m4a.tmp")
}
func isTidalHiResQuality(quality string) bool {
normalized := strings.TrimSpace(strings.ToUpper(quality))
return normalized == "HI_RES" || normalized == "HI_RES_LOSSLESS"
}
func buildTidalFilename(title, artist, album, albumArtist, releaseDate string, trackNumber, discNumber int, format string, includeTrackNumber bool, position int, useAlbumTrackNumber bool, extra ...string) string {