Fix GetTrackDownloadInfo to return signed download URLs

- Pass quality parameter to /tracks/{track_id}/download-info API request
- Add GetTrackDownloadURL method that resolves signed download URLs by
  downloading XML from download_info_url, parsing host/path/ts/s,
  computing MD5 signature (salt: XGRlBW9FXlekgbPrRHuSiA), and
  returning https://{host}/get-mp3/{sign}/{ts}{path}
- Add DownloadInfoURL field to DownloadInfo model for the XML URL
- Update DownloadTrack to use GetDirectLink resolution when URL is empty
- Port signing logic from yandex-music-api-python download_info.py
This commit is contained in:
Mrixs committed 2026-10-18 12:13:54 +03:00
1 parent dbb823ade3
commit ad83d84531
2 files changed
+94 -10

No files matched your search

+92 -10
View File
@@ -3,12 +3,47 @@ package client
import ( import (
"context" "context"
"crypto/md5"
"encoding/xml"
"fmt" "fmt"
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models" "gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils" "gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/utils"
) )
const downloadSignSalt = "XGRlBW9FXlekgbPrRHuSiA"
// downloadInfoXML represents the XML response from download_info_url.
type downloadInfoXML struct {
Host string `xml:"host"`
Path string `xml:"path"`
Ts string `xml:"ts"`
S string `xml:"s"`
}
// resolveDownloadURL downloads the XML from downloadInfoURL, parses host/path/ts/s,
// computes the MD5 signature, and returns the signed download URL:
// https://{host}/get-mp3/{sign}/{ts}{path}
func resolveDownloadURL(ctx context.Context, downloadInfoURL string, ri models.RequestInterface) (string, error) {
data, err := ri.Download(ctx, downloadInfoURL)
if err != nil {
return "", err
}
var xmlData downloadInfoXML
if err := xml.Unmarshal(data, &xmlData); err != nil {
return "", fmt.Errorf("failed to parse download info XML: %w", err)
}
if xmlData.Path == "" || xmlData.S == "" {
return "", utils.NewYandexMusicError("invalid download info XML: missing path or s", utils.ErrBadRequest)
}
sign := fmt.Sprintf("%x", md5.Sum([]byte(downloadSignSalt+xmlData.Path[1:]+xmlData.S)))
return fmt.Sprintf("https://%s/get-mp3/%s/%s%s", xmlData.Host, sign, xmlData.Ts, xmlData.Path), nil
}
// GetTracks gets tracks by their IDs (GET /tracks). // GetTracks gets tracks by their IDs (GET /tracks).
// Returns a slice of Track pointers, or nil if no IDs are provided. // Returns a slice of Track pointers, or nil if no IDs are provided.
func (c *Client) GetTracks(ctx context.Context, trackIDs []string) ([]*models.Track, error) { func (c *Client) GetTracks(ctx context.Context, trackIDs []string) ([]*models.Track, error) {
@@ -49,11 +84,12 @@ func (c *Client) GetTrack(ctx context.Context, trackID string) (*models.Track, e
// GetTrackDownloadInfo gets download info for a track (GET /tracks/{track_id}/download-info). // GetTrackDownloadInfo gets download info for a track (GET /tracks/{track_id}/download-info).
// The quality parameter specifies the desired audio quality (e.g., "mp3"). // The quality parameter specifies the desired audio quality (e.g., "mp3").
func (c *Client) GetTrackDownloadInfo(ctx context.Context, trackID, quality string) ([]models.DownloadInfo, error) { func (c *Client) GetTrackDownloadInfo(ctx context.Context, trackID, quality string) ([]models.DownloadInfo, error) {
// This requires signing - use the sign utility
// For now, return basic implementation
params := map[string]string{ params := map[string]string{
"track_id": trackID, "track_id": trackID,
} }
if quality != "" {
params["quality"] = quality
}
var result []models.DownloadInfo var result []models.DownloadInfo
if err := c.request.Get(ctx, fmt.Sprintf("/tracks/%s/download-info", trackID), params, &result); err != nil { if err := c.request.Get(ctx, fmt.Sprintf("/tracks/%s/download-info", trackID), params, &result); err != nil {
@@ -62,8 +98,45 @@ func (c *Client) GetTrackDownloadInfo(ctx context.Context, trackID, quality stri
return result, nil return result, nil
} }
// GetTrackDownloadURL returns a signed direct download URL for a track.
// It fetches download info, finds a matching entry, downloads the XML from its
// download_info_url, parses host/path/ts/s, computes the MD5 signature, and
// returns https://{host}/get-mp3/{sign}/{ts}{path}.
func (c *Client) GetTrackDownloadURL(ctx context.Context, trackID, format string, bitrate int) (string, error) {
info, err := c.GetTrackDownloadInfo(ctx, trackID, format)
if err != nil {
return "", err
}
// Find matching download info
var di *models.DownloadInfo
for i := range info {
if info[i].Codec == format && (bitrate == 0 || info[i].Bitrate == bitrate) {
di = &info[i]
break
}
}
if di == nil && len(info) > 0 {
di = &info[0]
}
if di == nil {
return "", utils.NewYandexMusicError("no download info available", utils.ErrNotFound)
}
if di.URL != "" {
return di.URL, nil
}
if di.DownloadInfoURL == "" {
return "", utils.NewYandexMusicError("no download URL available", utils.ErrNotFound)
}
return resolveDownloadURL(ctx, di.DownloadInfoURL, c.request)
}
// DownloadTrack downloads a track by ID. // DownloadTrack downloads a track by ID.
// It first fetches download info, finds a matching format/bitrate, then downloads the file content. // It first fetches download info, finds a matching format/bitrate, resolves the
// signed URL if needed, then downloads the file content.
func (c *Client) DownloadTrack(ctx context.Context, trackID, format string, bitrate int) ([]byte, error) { func (c *Client) DownloadTrack(ctx context.Context, trackID, format string, bitrate int) ([]byte, error) {
info, err := c.GetTrackDownloadInfo(ctx, trackID, format) info, err := c.GetTrackDownloadInfo(ctx, trackID, format)
if err != nil { if err != nil {
@@ -71,21 +144,30 @@ func (c *Client) DownloadTrack(ctx context.Context, trackID, format string, bitr
} }
// Find matching download info // Find matching download info
var url string var di *models.DownloadInfo
for _, di := range info { for i := range info {
if di.Codec == format && (bitrate == 0 || di.Bitrate == bitrate) { if info[i].Codec == format && (bitrate == 0 || info[i].Bitrate == bitrate) {
url = di.URL di = &info[i]
break break
} }
} }
if di == nil && len(info) > 0 {
if url == "" && len(info) > 0 { di = &info[0]
url = info[0].URL }
if di == nil {
return nil, utils.NewYandexMusicError("no download info available", utils.ErrNotFound)
} }
url := di.URL
if url == "" { if url == "" {
if di.DownloadInfoURL == "" {
return nil, utils.NewYandexMusicError("no download URL available", utils.ErrNotFound) return nil, utils.NewYandexMusicError("no download URL available", utils.ErrNotFound)
} }
url, err = resolveDownloadURL(ctx, di.DownloadInfoURL, c.request)
if err != nil {
return nil, err
}
}
return c.request.Download(ctx, url) return c.request.Download(ctx, url)
} }
+2
View File
@@ -171,4 +171,6 @@ type DownloadInfo struct {
Size int64 `json:"size"` Size int64 `json:"size"`
URL string `json:"url"` URL string `json:"url"`
DirectLink bool `json:"direct_link"` DirectLink bool `json:"direct_link"`
DownloadInfoURL string `json:"download_info_url"`
DirectLinkURL string `json:"-"` // Computed signed download URL
} }