api client implementation
This commit is contained in:
7
go.mod
7
go.mod
@@ -6,6 +6,11 @@ require (
|
||||
github.com/bogem/id3v2 v1.2.0
|
||||
github.com/caarlos0/env/v10 v10.0.0
|
||||
github.com/mattn/go-sqlite3 v1.14.28
|
||||
github.com/oapi-codegen/runtime v1.1.1
|
||||
)
|
||||
|
||||
require golang.org/x/text v0.3.2 // indirect
|
||||
require (
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/google/uuid v1.5.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
)
|
||||
|
||||
24
go.sum
24
go.sum
@@ -1,9 +1,31 @@
|
||||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||
github.com/bogem/id3v2 v1.2.0 h1:hKDF+F1gOgQ5r1QmBCEZUk4MveJbKxCeIDSBU7CQ4oI=
|
||||
github.com/bogem/id3v2 v1.2.0/go.mod h1:t78PK5AQ56Q47kizpYiV6gtjj3jfxlz87oFpty8DYs8=
|
||||
github.com/caarlos0/env/v10 v10.0.0 h1:yIHUBZGsyqCnpTkbjk8asUlx6RFhhEs+h7TOBdgdzXA=
|
||||
github.com/caarlos0/env/v10 v10.0.0/go.mod h1:ZfulV76NvVPw3tm591U4SwL3Xx9ldzBP9aGxzeN7G18=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
||||
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
|
||||
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
|
||||
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
198
pkg/yamusic/client.go
Normal file
198
pkg/yamusic/client.go
Normal file
@@ -0,0 +1,198 @@
|
||||
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
|
||||
}
|
||||
9863
pkg/yamusic/generated.go
Normal file
9863
pkg/yamusic/generated.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user