Migrate to yandex-music-api-go v0.0.0-20261018120746-dbf9a46bb2e7
CI/CD / test (push) Successful in 48s
CI/CD / build (push) Successful in 56s
CI/CD / docker (push) Successful in 2m37s

Library now provides GetTrackDownloadURL (signed URL via XML download_info_url),
so ym-dl delegates to it instead of reimplementing XML parsing and MD5 signing.
Removed local DownloadTrackXML/DownloadInfoXML and unused crypto/md5, encoding/hex,
encoding/xml, io imports from client.go.

Track.Lyrics replaced by LyricsInfo (availability flags only, no text).
Lyrics text requires separate GetTrackLyrics + GetTrackLyricsText calls —
not implemented yet, lyrics field left empty.
This commit is contained in:
Mrixs committed 2026-10-18 16:06:17 +03:00
1 parent d7692875db
commit 923705c7fc
4 files changed
+11 -111

No files matched your search

+1 -1
View File
@@ -3,7 +3,7 @@ module ym-dl
go 1.27.2 go 1.27.2
require ( require (
gitea.mrixs.me/Mrixs/yandex-music-api-go v0.0.0-20261018080126-dbb823ade349 gitea.mrixs.me/Mrixs/yandex-music-api-go v0.0.0-20261018120746-dbf9a46bb2e7
github.com/bogem/id3v2 v1.2.0 github.com/bogem/id3v2 v1.2.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.60.1 modernc.org/sqlite v1.60.1
+2
View File
@@ -6,6 +6,8 @@ gitea.mrixs.me/Mrixs/yandex-music-api-go v0.0.0-20261018072517-d0173f795d2a h1:y
gitea.mrixs.me/Mrixs/yandex-music-api-go v0.0.0-20261018072517-d0173f795d2a/go.mod h1:L+GcqPRZiu0Srq6+YxfdLztltXV8DcxKjNUOXWJvR9o= gitea.mrixs.me/Mrixs/yandex-music-api-go v0.0.0-20261018072517-d0173f795d2a/go.mod h1:L+GcqPRZiu0Srq6+YxfdLztltXV8DcxKjNUOXWJvR9o=
gitea.mrixs.me/Mrixs/yandex-music-api-go v0.0.0-20261018080126-dbb823ade349 h1:3Rru+kVwHrDZwO1AcJ6e/ia/O4XKt6A3Nux1WXmDBDE= gitea.mrixs.me/Mrixs/yandex-music-api-go v0.0.0-20261018080126-dbb823ade349 h1:3Rru+kVwHrDZwO1AcJ6e/ia/O4XKt6A3Nux1WXmDBDE=
gitea.mrixs.me/Mrixs/yandex-music-api-go v0.0.0-20261018080126-dbb823ade349/go.mod h1:L+GcqPRZiu0Srq6+YxfdLztltXV8DcxKjNUOXWJvR9o= gitea.mrixs.me/Mrixs/yandex-music-api-go v0.0.0-20261018080126-dbb823ade349/go.mod h1:L+GcqPRZiu0Srq6+YxfdLztltXV8DcxKjNUOXWJvR9o=
gitea.mrixs.me/Mrixs/yandex-music-api-go v0.0.0-20261018120746-dbf9a46bb2e7 h1:efp/oD3K2Pwm+fRuGKrQo/VMYKfj0VVenn1zvZEBB/0=
gitea.mrixs.me/Mrixs/yandex-music-api-go v0.0.0-20261018120746-dbf9a46bb2e7/go.mod h1:L+GcqPRZiu0Srq6+YxfdLztltXV8DcxKjNUOXWJvR9o=
github.com/bogem/id3v2 v1.2.0 h1:hKDF+F1gOgQ5r1QmBCEZUk4MveJbKxCeIDSBU7CQ4oI= github.com/bogem/id3v2 v1.2.0 h1:hKDF+F1gOgQ5r1QmBCEZUk4MveJbKxCeIDSBU7CQ4oI=
github.com/bogem/id3v2 v1.2.0/go.mod h1:t78PK5AQ56Q47kizpYiV6gtjj3jfxlz87oFpty8DYs8= github.com/bogem/id3v2 v1.2.0/go.mod h1:t78PK5AQ56Q47kizpYiV6gtjj3jfxlz87oFpty8DYs8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+5 -2
View File
@@ -584,9 +584,12 @@ func BuildTrackMetaFromYandex(track *yandexmusic.Track, album *yandexmusic.Album
} }
// Lyrics // Lyrics
// Note: track.LyricsInfo only contains availability flags, not the text.
// To fetch full lyrics text, call yandexClient.GetTrackLyrics + GetTrackLyricsText separately.
var lyrics string var lyrics string
if track.Lyrics != nil && track.Lyrics.HasText { if track.LyricsInfo != nil && track.LyricsInfo.HasAvailableTextLyrics {
lyrics = track.Lyrics.Text // Lyrics text available but not fetched here — requires separate API call
lyrics = ""
} }
return TrackMeta{ return TrackMeta{
+3 -108
View File
@@ -2,24 +2,17 @@ package yandex
import ( import (
"context" "context"
"crypto/md5"
"encoding/hex"
"encoding/xml"
"errors" "errors"
"fmt" "fmt"
"io"
"log/slog" "log/slog"
"net/http" "net/http"
"os" "os"
"strings"
"time" "time"
"ym-dl/internal/config" "ym-dl/internal/config"
"gitea.mrixs.me/Mrixs/yandex-music-api-go/yandexmusic" "gitea.mrixs.me/Mrixs/yandex-music-api-go/yandexmusic"
) )
const downloadSalt = "XGRlBW9FXlekgbPrRHuSiA" // Magic salt for URL signing
// TrackPosition represents track position in album // TrackPosition represents track position in album
type TrackPosition struct { type TrackPosition struct {
Volume int `json:"volume"` Volume int `json:"volume"`
@@ -158,106 +151,8 @@ func (c *YandexClient) GetArtistInfo(ctx context.Context, artistID int) (*yandex
return c.client.GetArtist(ctx, fmt.Sprintf("%d", artistID)) return c.client.GetArtist(ctx, fmt.Sprintf("%d", artistID))
} }
// DownloadTrackXML downloads the XML with track download URLs // GetTrackDownloadURL gets the direct MP3 download URL for a track.
func (c *YandexClient) DownloadTrackXML(ctx context.Context, downloadInfoURL string) ([]byte, error) { // Delegates to the library which handles XML download, parsing and URL signing.
req, err := http.NewRequestWithContext(ctx, "GET", downloadInfoURL, nil)
if err != nil {
return nil, fmt.Errorf("create download XML request: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("download XML request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("download XML failed: %s: %s", resp.Status, string(body))
}
return io.ReadAll(resp.Body)
}
// DownloadInfoXML describes the structure of the XML file with download info
type DownloadInfoXML struct {
XMLName xml.Name `xml:"download-info"`
Host string `xml:"host"`
Path string `xml:"path"`
Ts string `xml:"ts"`
S string `xml:"s"`
}
// GetTrackDownloadURL gets the direct MP3 download URL for a track
func (c *YandexClient) GetTrackDownloadURL(ctx context.Context, trackID string) (string, error) { func (c *YandexClient) GetTrackDownloadURL(ctx context.Context, trackID string) (string, error) {
// Get download info to find best MP3 quality return c.client.GetTrackDownloadURL(ctx, trackID, "mp3", 0)
downloadInfos, err := c.GetTrackDownloadInfo(ctx, trackID)
if err != nil {
return "", fmt.Errorf("get download info: %w", err)
}
// Find best quality MP3
var bestInfo *yandexmusic.DownloadInfo
for _, info := range downloadInfos {
if info.Codec == "mp3" && info.DirectLink {
if bestInfo == nil || info.Bitrate > bestInfo.Bitrate {
bestInfo = &info
}
}
}
if bestInfo == nil {
for _, info := range downloadInfos {
if info.Codec == "mp3" {
if bestInfo == nil || info.Bitrate > bestInfo.Bitrate {
bestInfo = &info
}
}
}
}
if bestInfo == nil && len(downloadInfos) > 0 {
bestInfo = &downloadInfos[0]
}
if bestInfo == nil {
return "", fmt.Errorf("no suitable download info found")
}
if c.debug {
for i, info := range downloadInfos {
c.logger.Debug("download info candidate", "index", i, "codec", info.Codec, "bitrate", info.Bitrate, "direct_link", info.DirectLink, "url", info.URL)
}
}
// If we have a direct URL, use it
if bestInfo.URL != "" && bestInfo.DirectLink {
return bestInfo.URL, nil
}
// If we have a download info URL, use it to get the signed URL
if bestInfo.URL != "" {
// Download and parse XML
xmlData, err := c.DownloadTrackXML(ctx, bestInfo.URL)
if err != nil {
return "", fmt.Errorf("download XML: %w", err)
}
var infoXML DownloadInfoXML
if err := xml.Unmarshal(xmlData, &infoXML); err != nil {
return "", fmt.Errorf("parse XML: %w", err)
}
// Generate signed URL
signData := []byte(downloadSalt + strings.TrimPrefix(infoXML.Path, "/") + infoXML.S)
sign := md5.Sum(signData)
hexSign := hex.EncodeToString(sign[:])
finalURL := fmt.Sprintf("https://%s/get-mp3/%s/%s%s", infoXML.Host, hexSign, infoXML.Ts, infoXML.Path)
if c.debug {
c.logger.Debug("constructed download URL", "url", finalURL)
}
return finalURL, nil
}
return "", fmt.Errorf("no download URL available: bestInfo.URL is empty (codec=%s, bitrate=%d, direct_link=%v)", bestInfo.Codec, bestInfo.Bitrate, bestInfo.DirectLink)
} }