Files
ym-dl-watcher/internal/downloader/downloader.go
T
Mrixs 4e6bef40e3
CI/CD / test (push) Successful in 48s
CI/CD / build (push) Successful in 54s
CI/CD / docker (push) Successful in 2m31s
Fix JSONID type conversions for yandex-music-api-go v0.0.0-20261018072517-d0173f795d2a
The new library version uses models.JSONID (custom string type) for ID
fields instead of plain string. Convert to string where IDs are passed
to functions expecting string or compared with string values.

Affected: client.go (GetTrackInfoWithPosition), downloader.go
(track.ID usage), monitor.go (track.ID, album.ID usage), storage.go
(album/track ID assignment).
2026-10-18 10:39:43 +03:00

261 lines
7.5 KiB
Go

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 string(track.ID) == "" || !track.Available {
continue
}
// Get download URL (handles XML parsing and URL signing)
downloadURL, err := d.yandexClient.GetTrackDownloadURL(ctx, string(track.ID))
if err != nil {
results = append(results, DownloadResult{TrackID: string(track.ID), Success: false, Error: fmt.Errorf("get download URL: %w", err)})
continue
}
// Get download info for metadata
downloadInfos, err := d.yandexClient.GetTrackDownloadInfo(ctx, string(track.ID))
if err != nil {
results = append(results, DownloadResult{TrackID: string(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: string(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: string(track.ID), Success: false, Error: fmt.Errorf("save track: %w", err)})
continue
}
savedMeta, _ := d.storage.GetTrackMeta(string(track.ID))
results = append(results, DownloadResult{
TrackID: string(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)
}