Track: Added FileInfo, FileDownloadInfo, TrackFullInfo, SimilarTracks types
Album: Added AlbumSimilarEntities, AlbumTrailer, SimilarEntityItem types
Artist: Added ArtistSimilar, ArtistLinks, ArtistAbout, ArtistInfo, ArtistSkeleton,
ArtistTrailerStatus, ArtistLink, Stats, SkeletonBlock types
Client methods added:
- artists.go: GetArtistBriefInfo, GetArtistSimilar, GetArtistLinks, GetArtistAlsoAlbums,
GetArtistDiscographyAlbums, GetArtistSafeDirectAlbums, GetArtistTrackIds, GetArtistAbout,
GetArtistDonation, GetArtistInfo, GetArtistSkeleton, GetArtistTrailer
- albums.go: GetAlbumSimilarEntities, GetAlbumTrailer
- tracks.go: GetTracksFileInfo, GetTracksSimilar, GetTrackTrailer, GetTrackFullInfo
248 lines
7.2 KiB
Go
248 lines
7.2 KiB
Go
// Package client provides methods for interacting with Yandex Music API endpoints.
|
|
package client
|
|
|
|
import (
|
|
"context"
|
|
"crypto/md5"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/models"
|
|
"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).
|
|
// 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) {
|
|
if len(trackIDs) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
params := map[string]string{
|
|
"track-ids": joinIDs(trackIDs),
|
|
}
|
|
|
|
var result []models.Track
|
|
if err := c.request.Get(ctx, "/tracks", params, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tracks := make([]*models.Track, len(result))
|
|
for i := range result {
|
|
result[i].SetClient(c)
|
|
tracks[i] = &result[i]
|
|
}
|
|
return tracks, nil
|
|
}
|
|
|
|
// GetTrack gets a single track by ID using GetTracks.
|
|
// Returns an error if no track is found with the given ID.
|
|
func (c *Client) GetTrack(ctx context.Context, trackID string) (*models.Track, error) {
|
|
tracks, err := c.GetTracks(ctx, []string{trackID})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(tracks) == 0 {
|
|
return nil, utils.NewYandexMusicError("track not found", utils.ErrNotFound)
|
|
}
|
|
return tracks[0], nil
|
|
}
|
|
|
|
// GetTrackDownloadInfo gets download info for a track (GET /tracks/{track_id}/download-info).
|
|
// The quality parameter specifies the desired audio quality (e.g., "mp3").
|
|
func (c *Client) GetTrackDownloadInfo(ctx context.Context, trackID, quality string) ([]models.DownloadInfo, error) {
|
|
params := map[string]string{
|
|
"track_id": trackID,
|
|
}
|
|
if quality != "" {
|
|
params["quality"] = quality
|
|
}
|
|
|
|
var result []models.DownloadInfo
|
|
if err := c.request.Get(ctx, fmt.Sprintf("/tracks/%s/download-info", trackID), params, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
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.
|
|
// 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) {
|
|
info, err := c.GetTrackDownloadInfo(ctx, trackID, format)
|
|
if err != nil {
|
|
return nil, 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 nil, utils.NewYandexMusicError("no download info available", utils.ErrNotFound)
|
|
}
|
|
|
|
url := di.URL
|
|
if url == "" {
|
|
if di.DownloadInfoURL == "" {
|
|
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)
|
|
}
|
|
|
|
// GetTracksFileInfo gets file info for a track (GET /get-file-info).
|
|
func (c *Client) GetTracksFileInfo(ctx context.Context, trackID, quality string, codecs []string, transport string) (*models.FileInfo, error) {
|
|
params := map[string]string{
|
|
"track_id": trackID,
|
|
"quality": quality,
|
|
}
|
|
if len(codecs) > 0 {
|
|
params["codecs"] = strings.Join(codecs, ",")
|
|
}
|
|
if transport != "" {
|
|
params["transport"] = transport
|
|
}
|
|
|
|
var result models.FileInfo
|
|
if err := c.request.Get(ctx, "/get-file-info", params, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
result.SetClient(c)
|
|
return &result, nil
|
|
}
|
|
|
|
// GetTracksSimilar gets similar tracks (GET /tracks/{track_id}/similar).
|
|
func (c *Client) GetTracksSimilar(ctx context.Context, trackID string) (*models.SimilarTracks, error) {
|
|
var result models.SimilarTracks
|
|
if err := c.request.Get(ctx, fmt.Sprintf("/tracks/%s/similar", trackID), nil, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
result.SetClient(c)
|
|
return &result, nil
|
|
}
|
|
|
|
// GetTrackTrailer gets track trailer (GET /tracks/{track_id}/trailer).
|
|
func (c *Client) GetTrackTrailer(ctx context.Context, trackID string) (*models.TrackTrailer, error) {
|
|
var result models.TrackTrailer
|
|
if err := c.request.Get(ctx, fmt.Sprintf("/tracks/%s/trailer", trackID), nil, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
result.SetClient(c)
|
|
return &result, nil
|
|
}
|
|
|
|
// GetTrackFullInfo gets full track info (GET /tracks/{track_id}/full-info).
|
|
func (c *Client) GetTrackFullInfo(ctx context.Context, trackID string) (*models.TrackFullInfo, error) {
|
|
var result models.TrackFullInfo
|
|
if err := c.request.Get(ctx, fmt.Sprintf("/tracks/%s/full-info", trackID), nil, &result); err != nil {
|
|
return nil, err
|
|
}
|
|
result.SetClient(c)
|
|
return &result, nil
|
|
}
|
|
|
|
// SearchResult represents search results containing tracks.
|
|
type SearchResult struct {
|
|
Tracks *SearchTracksResult `json:"tracks"`
|
|
}
|
|
|
|
// SearchTracksResult represents track search results with total count and results list.
|
|
type SearchTracksResult struct {
|
|
Total int `json:"total"`
|
|
Results []models.Track `json:"results"`
|
|
}
|
|
|
|
func joinIDs(ids []string) string {
|
|
result := ""
|
|
for i, id := range ids {
|
|
if i > 0 {
|
|
result += ","
|
|
}
|
|
result += id
|
|
}
|
|
return result
|
|
}
|