Initial commit: ym-dl with yandex-music-api-go library migration
Migrate ym-dl from custom oapi-codegen Yandex Music API client to external gitea.mrixs.me/Mrixs/yandex-music-api-go library with token-only authentication. Key changes: - Replace custom Yandex API client with yandexmusic library wrapper - Token-based OAuth authentication only (no username/password fallback) - SQLite storage index with JSON index migration path - Extended ID3 tag support: genre, comment, lyrics, publisher, ISRC, copyright - High-resolution album covers (1000x1000) - Fixed album download check (requires all tracks, not just one)
This commit is contained in:
commit
fe62910311
16 files changed
+6688
No files matched your search
@@ -0,0 +1,261 @@
|
||||
package downloader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ym-dl/internal/storage"
|
||||
"ym-dl/internal/yandex"
|
||||
"gitea.mrixs.me/Mrixs/yandex-music-api-go/yandexmusic"
|
||||
)
|
||||
|
||||
// DownloadResult represents the result of a download operation
|
||||
type DownloadResult struct {
|
||||
TrackID string
|
||||
AlbumID string
|
||||
Success bool
|
||||
Error error
|
||||
TrackMeta storage.TrackMeta
|
||||
AlbumMeta storage.AlbumMeta
|
||||
}
|
||||
|
||||
// Downloader orchestrates the download of tracks and albums
|
||||
type Downloader struct {
|
||||
yandexClient *yandex.YandexClient
|
||||
storage *storage.Storage
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewDownloader creates a new downloader
|
||||
func NewDownloader(yandexClient *yandex.YandexClient, storage *storage.Storage) *Downloader {
|
||||
return &Downloader{
|
||||
yandexClient: yandexClient,
|
||||
storage: storage,
|
||||
httpClient: &http.Client{Timeout: 5 * 60 * time.Second}, // 5 min timeout for large files
|
||||
}
|
||||
}
|
||||
|
||||
// DownloadTrack downloads a single track
|
||||
func (d *Downloader) DownloadTrack(ctx context.Context, trackID string) DownloadResult {
|
||||
// Check if already downloaded
|
||||
if d.storage.IsTrackDownloaded(trackID) {
|
||||
meta, _ := d.storage.GetTrackMeta(trackID)
|
||||
return DownloadResult{
|
||||
TrackID: trackID,
|
||||
Success: true,
|
||||
TrackMeta: meta,
|
||||
}
|
||||
}
|
||||
|
||||
// Get track info from API with position
|
||||
track, position, err := d.yandexClient.GetTrackInfoWithPosition(ctx, trackID)
|
||||
if err != nil {
|
||||
return DownloadResult{TrackID: trackID, Success: false, Error: fmt.Errorf("get track info: %w", err)}
|
||||
}
|
||||
|
||||
// Get album info if available
|
||||
var album *yandexmusic.Album
|
||||
var coverData []byte
|
||||
if len(track.Albums) > 0 {
|
||||
album = &track.Albums[0]
|
||||
// Fetch album cover
|
||||
if album.CoverURI != "" {
|
||||
coverURL := strings.Replace(album.CoverURI, "%%", "1000x1000", 1)
|
||||
coverURL = "https://" + strings.TrimPrefix(coverURL, "//")
|
||||
coverData, _ = d.downloadFile(ctx, coverURL)
|
||||
}
|
||||
}
|
||||
|
||||
// Determine track number and disc number from API response
|
||||
trackNum := 1
|
||||
discNum := 1
|
||||
if position != nil {
|
||||
trackNum = position.Index
|
||||
discNum = position.Volume
|
||||
}
|
||||
|
||||
// Get direct download URL (handles XML parsing and URL signing)
|
||||
downloadURL, err := d.yandexClient.GetTrackDownloadURL(ctx, trackID)
|
||||
if err != nil {
|
||||
return DownloadResult{TrackID: trackID, Success: false, Error: fmt.Errorf("get download URL: %w", err)}
|
||||
}
|
||||
|
||||
// Download the actual audio file
|
||||
audioData, err := d.downloadFile(ctx, downloadURL)
|
||||
if err != nil {
|
||||
return DownloadResult{TrackID: trackID, Success: false, Error: fmt.Errorf("download audio: %w", err)}
|
||||
}
|
||||
|
||||
// Get download info for metadata (codec, bitrate)
|
||||
downloadInfos, err := d.yandexClient.GetTrackDownloadInfo(ctx, trackID)
|
||||
if err != nil {
|
||||
return DownloadResult{TrackID: trackID, Success: false, Error: fmt.Errorf("get download info: %w", err)}
|
||||
}
|
||||
|
||||
// Find best quality MP3 for metadata
|
||||
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]
|
||||
}
|
||||
|
||||
// Build track meta with full metadata
|
||||
trackMeta := storage.BuildTrackMetaFromYandex(track, album, trackNum, discNum, *bestInfo)
|
||||
|
||||
if err := d.storage.SaveTrack(trackMeta, audioData, coverData); err != nil {
|
||||
return DownloadResult{TrackID: trackID, Success: false, Error: fmt.Errorf("save track: %w", err)}
|
||||
}
|
||||
|
||||
savedMeta, _ := d.storage.GetTrackMeta(trackID)
|
||||
return DownloadResult{
|
||||
TrackID: trackID,
|
||||
Success: true,
|
||||
TrackMeta: savedMeta,
|
||||
}
|
||||
}
|
||||
|
||||
// DownloadAlbum downloads all tracks from an album
|
||||
func (d *Downloader) DownloadAlbum(ctx context.Context, albumID string) []DownloadResult {
|
||||
var results []DownloadResult
|
||||
|
||||
// Get album with tracks
|
||||
album, err := d.yandexClient.GetAlbumWithTracks(ctx, albumID)
|
||||
if err != nil {
|
||||
return []DownloadResult{{AlbumID: albumID, Success: false, Error: fmt.Errorf("get album: %w", err)}}
|
||||
}
|
||||
|
||||
// Get album cover
|
||||
var coverData []byte
|
||||
if album.CoverURI != "" {
|
||||
coverURL := strings.Replace(album.CoverURI, "%%", "1000x1000", 1)
|
||||
coverURL = "https://" + strings.TrimPrefix(coverURL, "//")
|
||||
coverData, _ = d.downloadFile(ctx, coverURL)
|
||||
}
|
||||
|
||||
// Build track metas
|
||||
var trackMetas []storage.TrackMeta
|
||||
if album.Volumes != nil {
|
||||
for i, volume := range album.Volumes {
|
||||
for j := range volume {
|
||||
track := &volume[j]
|
||||
if track.ID == "" || !track.Available {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get download URL (handles XML parsing and URL signing)
|
||||
downloadURL, err := d.yandexClient.GetTrackDownloadURL(ctx, track.ID)
|
||||
if err != nil {
|
||||
results = append(results, DownloadResult{TrackID: track.ID, Success: false, Error: fmt.Errorf("get download URL: %w", err)})
|
||||
continue
|
||||
}
|
||||
|
||||
// Get download info for metadata
|
||||
downloadInfos, err := d.yandexClient.GetTrackDownloadInfo(ctx, track.ID)
|
||||
if err != nil {
|
||||
results = append(results, DownloadResult{TrackID: track.ID, Success: false, Error: fmt.Errorf("get download info: %w", err)})
|
||||
continue
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
trackMeta := storage.BuildTrackMetaFromYandex(track, album, j+1, i+1, *bestInfo)
|
||||
trackMetas = append(trackMetas, trackMeta)
|
||||
|
||||
// Download audio
|
||||
audioData, err := d.downloadFile(ctx, downloadURL)
|
||||
if err != nil {
|
||||
results = append(results, DownloadResult{TrackID: track.ID, Success: false, Error: fmt.Errorf("download audio: %w", err)})
|
||||
continue
|
||||
}
|
||||
|
||||
// Save track
|
||||
if err := d.storage.SaveTrack(trackMeta, audioData, coverData); err != nil {
|
||||
results = append(results, DownloadResult{TrackID: track.ID, Success: false, Error: fmt.Errorf("save track: %w", err)})
|
||||
continue
|
||||
}
|
||||
|
||||
savedMeta, _ := d.storage.GetTrackMeta(track.ID)
|
||||
results = append(results, DownloadResult{
|
||||
TrackID: track.ID,
|
||||
AlbumID: albumID,
|
||||
Success: true,
|
||||
TrackMeta: savedMeta,
|
||||
})
|
||||
|
||||
// Delay between downloads to be respectful to the API
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return results
|
||||
case <-time.After(1 * time.Second):
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save album meta
|
||||
albumMeta := storage.BuildAlbumMetaFromYandex(album, trackMetas)
|
||||
if err := d.storage.SaveAlbum(albumMeta, coverData); err != nil {
|
||||
results = append(results, DownloadResult{AlbumID: albumID, Success: false, Error: fmt.Errorf("save album: %w", err)})
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// downloadFile downloads a file from a URL
|
||||
func (d *Downloader) downloadFile(ctx context.Context, url string) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user