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) }