noot
All checks were successful
builder / build (push) Successful in 49s

This commit is contained in:
a 2024-12-02 22:01:58 -06:00
parent e4f23c6805
commit 9a34c4b05b
No known key found for this signature in database
GPG Key ID: 2F22877AA4DFDADB
5 changed files with 296 additions and 38 deletions

1
go.mod
View File

@ -8,6 +8,7 @@ require (
github.com/charmbracelet/bubbles v0.20.0
github.com/charmbracelet/bubbletea v1.1.2
github.com/charmbracelet/lipgloss v0.13.1
github.com/go-resty/resty/v2 v2.16.2
github.com/lmittmann/tint v1.0.5
github.com/rivo/tview v0.0.0-20241016194538-c5e4fb24af13
github.com/urfave/cli/v3 v3.0.0-alpha9.1

4
go.sum
View File

@ -94,6 +94,8 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg=
github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -418,6 +420,8 @@ golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=

View File

@ -18,18 +18,18 @@ import (
func (c *Commander) Radio() error {
currentSong, err := c.Client().PlayerCurrentlyPlaying(c.Context)
if err != nil {
return err
return fmt.Errorf("failed to get current song: %w", err)
}
if currentSong.Item != nil {
return c.RadioGivenSong(currentSong.Item.SimpleTrack, currentSong.Progress)
}
_, err = c.activateDevice()
if err != nil {
return err
return fmt.Errorf("failed to activate device: %w", err)
}
tracks, err := c.Client().CurrentUsersTracks(c.Context, spotify.Limit(10))
if err != nil {
return err
return fmt.Errorf("failed to get current users tracks: %w", err)
}
return c.RadioGivenSong(tracks.Tracks[rand.Intn(len(tracks.Tracks))].SimpleTrack, 0)
}
@ -178,7 +178,7 @@ func (c *Commander) RadioGivenSong(song spotify.SimpleTrack, pos spotify.Numeric
}
recomendations, err := c.Client().GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(99))
if err != nil {
return err
return fmt.Errorf("failed to get recommendations: %w", err)
}
recomendationIds := []spotify.ID{}
for _, song := range recomendations.Tracks {
@ -218,26 +218,10 @@ func (c *Commander) RadioGivenSong(song spotify.SimpleTrack, pos spotify.Numeric
if pos != 0 {
pos = pos + spotify.Numeric(delay)
}
err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackContext: &radioPlaylist.URI,
PositionMs: pos,
})
if err != nil {
if isNoActiveError(err) {
deviceID, err := c.activateDevice()
err = c.PlayRadio(radioPlaylist, int(pos))
if err != nil {
return err
}
err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackContext: &radioPlaylist.URI,
DeviceID: &deviceID,
PositionMs: pos,
})
if err != nil {
return err
}
}
}
err = c.Client().Repeat(c.Context, "context")
if err != nil {
return err
@ -274,6 +258,30 @@ func (c *Commander) RadioGivenSong(song spotify.SimpleTrack, pos spotify.Numeric
return nil
}
func (c *Commander) PlayRadio(radioPlaylist *spotify.FullPlaylist, pos int) error {
err := c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackContext: &radioPlaylist.URI,
PositionMs: spotify.Numeric(pos),
})
if err != nil {
if isNoActiveError(err) {
deviceID, err := c.activateDevice()
if err != nil {
return err
}
err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackContext: &radioPlaylist.URI,
DeviceID: &deviceID,
PositionMs: spotify.Numeric(pos),
})
if err != nil {
return err
}
}
}
return nil
}
func (c *Commander) ClearRadio() error {
radioPlaylist, db, err := c.GetRadioPlaylist("")
if err != nil {
@ -581,24 +589,10 @@ func (c *Commander) RadioGivenList(songs []spotify.ID, name string) error {
if err != nil {
return err
}
err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackContext: &radioPlaylist.URI,
})
if err != nil {
if isNoActiveError(err) {
deviceID, err := c.activateDevice()
err = c.PlayRadio(radioPlaylist, 0)
if err != nil {
return err
}
err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
PlaybackContext: &radioPlaylist.URI,
DeviceID: &deviceID,
})
if err != nil {
return err
}
}
}
for i := 0; i < 4; i++ {
id := rand.Intn(len(recomendationIds)-2) + 1
seed := spotify.Seeds{

View File

@ -0,0 +1,180 @@
package listenbrainz
import (
"context"
"fmt"
"github.com/go-resty/resty/v2"
)
type ListenBrainz struct {
client *resty.Client
labs *resty.Client
}
func NewListenBrainz(
Endpoint string,
ApiKey string,
) *ListenBrainz {
c := resty.New().SetBaseURL(Endpoint)
if ApiKey != "" {
c = c.SetHeader("Authorization", "Token "+ApiKey)
}
return &ListenBrainz{
client: c,
labs: resty.New().SetBaseURL("https://labs.api.listenbrainz.org"),
}
}
type RadioApiResponse struct {
Payload struct {
Feedback []string `json:"feedback"`
Jspf struct {
Playlist struct {
Annotation string `json:"annotation"`
Creator string `json:"creator"`
Extension struct {
HTTPSMusicbrainzOrgDocJspfPlaylist struct {
Public bool `json:"public"`
} `json:"https://musicbrainz.org/doc/jspf#playlist"`
} `json:"extension"`
Title string `json:"title"`
Track []ApiTrack `json:"track"`
} `json:"playlist"`
} `json:"jspf"`
} `json:"payload"`
}
type ApiTrack struct {
Album string `json:"album"`
Creator string `json:"creator"`
Duration int `json:"duration,omitempty"`
Extension struct {
HTTPSMusicbrainzOrgDocJspfTrack struct {
ArtistIdentifiers []string `json:"artist_identifiers"`
ReleaseIdentifier string `json:"release_identifier"`
} `json:"https://musicbrainz.org/doc/jspf#track"`
} `json:"extension"`
Identifier []string `json:"identifier"`
Title string `json:"title"`
}
func (o *ListenBrainz) RequestRadio(ctx context.Context, req *RadioRequest) (*RadioTracksResponse, error) {
var res RadioApiResponse
resp, err := o.client.R().
SetResult(&res).
SetQueryParam("prompt", req.Prompt).
SetQueryParam("mode", req.Prompt).
Get("/1/explore/lb-radio")
if err != nil {
return nil, err
}
switch {
case resp.StatusCode() == 200:
default:
return nil, fmt.Errorf("radio request code %d: %s", resp.StatusCode(), resp.Status())
}
tracks := res.Payload.Jspf.Playlist.Track
return &RadioTracksResponse{
Tracks: tracks,
}, nil
}
type RadioTracksResponse struct {
Tracks []ApiTrack
}
type RadioRequest struct {
Prompt string
Mode string `json:"mode"`
}
type MatchTracksParams struct {
Tracks []ApiTrack
}
func (o *ListenBrainz) MatchTracks(ctx context.Context, params *MatchTracksParams) error {
// first try to get the mbid from the recording id
o.labs.R().Get("/")
return nil
}
type MatchedTrack struct {
Mbid string
SpotifyId string
Strategy string
}
type TrackMatch struct {
RecordingMbid string `json:"recording_mbid"`
ArtistName string `json:"artist_name"`
ReleaseName string `json:"release_name"`
TrackName string `json:"track_name"`
SpotifyTrackIds []string `json:"spotify_track_ids"`
}
var ErrNoMatch = fmt.Errorf("no match")
func (o *ListenBrainz) MatchTrack(ctx context.Context, track *ApiTrack) (*MatchedTrack, error) {
// refuse to match a track with no identifiers
if len(track.Identifier) == 0 {
return nil, fmt.Errorf("%w: no identifier", ErrNoMatch)
}
var matches []TrackMatch
// there are mbids, so try to get the first one that is valid
resp, err := o.labs.R().
SetResult(&matches).
SetQueryParam("recording_mbid", track.Identifier[0]).
Get("/spotify-id-from-mbid/json")
if err != nil {
return nil, err
}
if resp.StatusCode() != 200 {
return nil, fmt.Errorf("labs request code %d: %s", resp.StatusCode(), resp.Status())
}
if len(matches) == 0 {
return nil, fmt.Errorf("%w: no mbid", ErrNoMatch)
}
// for each match, see if ther eis a spotify id, and if there is, we are done!
for _, match := range matches {
if len(match.SpotifyTrackIds) == 0 {
continue
}
return &MatchedTrack{
Mbid: match.RecordingMbid,
SpotifyId: match.SpotifyTrackIds[0],
Strategy: "exact-match",
}, nil
}
for _, match := range matches {
var submatch []TrackMatch
resp, err := o.labs.R().
SetResult(&submatch).
SetQueryParam("artist_name", match.ArtistName).
SetQueryParam("release_name", match.ReleaseName).
SetQueryParam("track_name", match.TrackName).
Get("/spotify-id-from-track/json")
if err != nil {
return nil, err
}
if resp.StatusCode() != 200 {
return nil, fmt.Errorf("labs request code %d: %s", resp.StatusCode(), resp.Status())
}
if len(submatch) == 0 {
return nil, fmt.Errorf("%w: no tracks found", ErrNoMatch)
}
for _, submatch := range submatch {
if len(submatch.SpotifyTrackIds) == 0 {
continue
}
return &MatchedTrack{
Mbid: match.RecordingMbid,
SpotifyId: submatch.SpotifyTrackIds[0],
Strategy: "track-match",
}, nil
}
}
return nil, fmt.Errorf("%w: no tracks found", ErrNoMatch)
}

79
src/listenbrainz/radio.go Normal file
View File

@ -0,0 +1,79 @@
package listenbrainz
import (
"bytes"
"strconv"
)
type RadioPromptBuilder struct {
px RadioParameters
}
func (o *RadioPromptBuilder) Add(name string, values ...string) *RadioPromptBuilder {
o.px = append(o.px, RadioParameter{
Name: name,
Values: values,
})
return o
}
func (o *RadioPromptBuilder) AddParameter(p RadioParameter) *RadioPromptBuilder {
o.px = append(o.px, p)
return o
}
func (o *RadioPromptBuilder) AddWithCount(name string, count int, values ...string) *RadioPromptBuilder {
o.px = append(o.px, RadioParameter{
Name: name,
Count: count,
Values: values,
})
return o
}
func (o *RadioPromptBuilder) AddWithOption(name string, option string, values ...string) *RadioPromptBuilder {
o.px = append(o.px, RadioParameter{
Name: name,
Option: option,
Values: values,
})
return o
}
func (o *RadioPromptBuilder) String() string {
val, _ := o.px.MarshalText()
return string(val)
}
type RadioParameters []RadioParameter
func (r RadioParameters) MarshalText() ([]byte, error) {
o := &bytes.Buffer{}
for pidx, v := range r {
o.WriteString(v.Name)
o.WriteString(":(")
for idx, vv := range v.Values {
o.WriteString(vv)
if len(v.Values) > 1 && idx != len(v.Values)-1 {
o.WriteString(",")
}
}
o.WriteString(")")
if v.Count > 0 {
o.WriteString(":" + strconv.Itoa(v.Count))
}
if v.Option != "" {
o.WriteString(":" + v.Option)
}
if len(r) > 1 && pidx != len(r)-1 {
o.WriteString(" ")
}
}
return o.Bytes(), nil
}
type RadioParameter struct {
Name string `json:"name"`
Values []string `json:"value"`
Count int `json:"count"`
Option string `json:"options"`
}