199 lines
6.3 KiB
Go
199 lines
6.3 KiB
Go
package yamusic
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gitea.mrixs.me/Mrixs/yamusic-bot/internal/model"
|
|
)
|
|
|
|
const (
|
|
yandexMusicAPIHost = "https://api.music.yandex.net"
|
|
)
|
|
|
|
var (
|
|
trackURLRegex = regexp.MustCompile(`/album/(\d+)/track/(\d+)`)
|
|
albumURLRegex = regexp.MustCompile(`/album/(\d+)`)
|
|
artistURLRegex = regexp.MustCompile(`/artist/(\d+)`)
|
|
)
|
|
|
|
type URLInfo struct {
|
|
Type string
|
|
ArtistID string
|
|
AlbumID string
|
|
TrackID string
|
|
}
|
|
|
|
func ParseYandexURL(url string) (*URLInfo, error) {
|
|
if trackMatches := trackURLRegex.FindStringSubmatch(url); len(trackMatches) == 3 {
|
|
return &URLInfo{Type: "track", AlbumID: trackMatches[1], TrackID: trackMatches[2]}, nil
|
|
}
|
|
if albumMatches := albumURLRegex.FindStringSubmatch(url); len(albumMatches) == 2 {
|
|
return &URLInfo{Type: "album", AlbumID: albumMatches[1]}, nil
|
|
}
|
|
if artistMatches := artistURLRegex.FindStringSubmatch(url); len(artistMatches) == 2 {
|
|
return &URLInfo{Type: "artist", ArtistID: artistMatches[1]}, nil
|
|
}
|
|
return nil, fmt.Errorf("unsupported yandex music url")
|
|
}
|
|
|
|
type ApiClient struct {
|
|
api *ClientWithResponses
|
|
}
|
|
|
|
func NewApiClient(token string) (*ApiClient, error) {
|
|
authInterceptor := func(ctx context.Context, req *http.Request) error {
|
|
if token != "" {
|
|
req.Header.Set("Authorization", "OAuth "+token)
|
|
}
|
|
return nil
|
|
}
|
|
apiClient, err := NewClientWithResponses(yandexMusicAPIHost, WithRequestEditorFn(authInterceptor))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create yandex music api client: %w", err)
|
|
}
|
|
return &ApiClient{api: apiClient}, nil
|
|
}
|
|
|
|
func (c *ApiClient) GetTrackInfo(ctx context.Context, trackID string) (*model.TrackInfo, error) {
|
|
body := GetTracksFormdataRequestBody{TrackIds: &[]string{trackID}}
|
|
resp, err := c.api.GetTracksWithFormdataBodyWithResponse(ctx, body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get track info from api: %w", err)
|
|
}
|
|
if resp.StatusCode() != http.StatusOK || resp.JSON200 == nil || resp.JSON200.Result == nil || len(resp.JSON200.Result) == 0 {
|
|
return nil, fmt.Errorf("failed to get track info, status: %d, body: %s", resp.StatusCode(), string(resp.Body))
|
|
}
|
|
|
|
track := (resp.JSON200.Result)[0]
|
|
return c.convertTrackToTrackInfo(&track)
|
|
}
|
|
|
|
func (c *ApiClient) GetAlbumTrackInfos(ctx context.Context, albumID string) ([]*model.TrackInfo, error) {
|
|
albumIDFloat, _ := strconv.ParseFloat(albumID, 32)
|
|
resp, err := c.api.GetAlbumsWithTracksWithResponse(ctx, float32(albumIDFloat))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get album info from api: %w", err)
|
|
}
|
|
if resp.StatusCode() != http.StatusOK || resp.JSON200 == nil || resp.JSON200.Result.Id == 0 {
|
|
return nil, fmt.Errorf("failed to get album info, status: %d, body: %s", resp.StatusCode(), string(resp.Body))
|
|
}
|
|
|
|
album := resp.JSON200.Result
|
|
var trackInfos []*model.TrackInfo
|
|
if album.Volumes != nil {
|
|
trackNum := 1
|
|
for _, volume := range *album.Volumes {
|
|
for _, track := range volume {
|
|
info, err := c.convertTrackToTrackInfo(&track)
|
|
if err != nil {
|
|
slog.Warn("Failed to convert track, skipping", "trackID", track.Id, "error", err)
|
|
continue
|
|
}
|
|
info.TrackPosition = trackNum
|
|
trackNum++
|
|
trackInfos = append(trackInfos, info)
|
|
}
|
|
}
|
|
}
|
|
return trackInfos, nil
|
|
}
|
|
|
|
func (c *ApiClient) GetArtistTrackInfos(ctx context.Context, artistID string) ([]*model.TrackInfo, error) {
|
|
resp, err := c.api.GetPopularTracksWithResponse(ctx, artistID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get artist popular tracks from api: %w", err)
|
|
}
|
|
if resp.StatusCode() != http.StatusOK || resp.JSON200 == nil {
|
|
return nil, fmt.Errorf("failed to get artist popular tracks, status: %d, body: %s", resp.StatusCode(), string(resp.Body))
|
|
}
|
|
|
|
trackIDs := resp.JSON200.Result.Tracks
|
|
if len(trackIDs) == 0 {
|
|
return []*model.TrackInfo{}, nil
|
|
}
|
|
|
|
body := GetTracksFormdataRequestBody{TrackIds: &trackIDs}
|
|
tracksResp, err := c.api.GetTracksWithFormdataBodyWithResponse(ctx, body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get full info for popular tracks: %w", err)
|
|
}
|
|
if tracksResp.StatusCode() != http.StatusOK || tracksResp.JSON200 == nil || tracksResp.JSON200.Result == nil {
|
|
return nil, fmt.Errorf("failed to get full info for popular tracks, status: %d, body: %s", tracksResp.StatusCode(), string(tracksResp.Body))
|
|
}
|
|
|
|
var trackInfos []*model.TrackInfo
|
|
for _, track := range tracksResp.JSON200.Result {
|
|
info, err := c.convertTrackToTrackInfo(&track)
|
|
if err != nil {
|
|
slog.Warn("Failed to convert track, skipping", "trackID", track.Id, "error", err)
|
|
continue
|
|
}
|
|
trackInfos = append(trackInfos, info)
|
|
}
|
|
return trackInfos, nil
|
|
}
|
|
|
|
func (c *ApiClient) GetDownloadURL(ctx context.Context, trackID string) (string, error) {
|
|
resp, err := c.api.GetDownloadInfoWithResponse(ctx, trackID)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get download info from api: %w", err)
|
|
}
|
|
if resp.StatusCode() != http.StatusOK || resp.JSON200 == nil || resp.JSON200.Result == nil || len(resp.JSON200.Result) == 0 {
|
|
return "", fmt.Errorf("failed to get download info, status: %d, body: %s", resp.StatusCode(), string(resp.Body))
|
|
}
|
|
|
|
var bestURL string
|
|
var maxBitrate int = 0
|
|
for _, info := range resp.JSON200.Result {
|
|
if info.Codec == "mp3" && int(info.BitrateInKbps) > maxBitrate {
|
|
maxBitrate = int(info.BitrateInKbps)
|
|
bestURL = info.DownloadInfoUrl
|
|
}
|
|
}
|
|
|
|
if bestURL == "" {
|
|
return "", fmt.Errorf("no suitable mp3 download link found for track %s", trackID)
|
|
}
|
|
|
|
slog.Warn("Returning XML info URL instead of direct download link. Real implementation needed.", "url", bestURL)
|
|
return "https://example.com/download/track.mp3", nil
|
|
}
|
|
|
|
func (c *ApiClient) convertTrackToTrackInfo(track *Track) (*model.TrackInfo, error) {
|
|
if track == nil || track.Id == "" {
|
|
return nil, fmt.Errorf("invalid track data")
|
|
}
|
|
|
|
info := &model.TrackInfo{
|
|
YandexTrackID: track.Id,
|
|
Title: track.Title,
|
|
}
|
|
|
|
if len(track.Artists) > 0 {
|
|
var artists []string
|
|
for _, artist := range track.Artists {
|
|
artists = append(artists, artist.Name)
|
|
}
|
|
info.Artist = strings.Join(artists, ", ")
|
|
}
|
|
|
|
if len(track.Albums) > 0 {
|
|
album := track.Albums[0]
|
|
info.YandexAlbumID = strconv.FormatFloat(float64(album.Id), 'f', -1, 32)
|
|
info.Album = album.Title
|
|
info.Year = int(album.Year)
|
|
info.Genre = album.Genre
|
|
}
|
|
|
|
info.CoverURL = "https://" + strings.Replace(track.CoverUri, "%%", "400x400", 1)
|
|
info.DownloadURL = ""
|
|
|
|
return info, nil
|
|
}
|