From 9a34c4b05b0ea04c644668b0aac46429c6ac1ab1 Mon Sep 17 00:00:00 2001
From: a
Date: Mon, 2 Dec 2024 22:01:58 -0600
Subject: [PATCH 1/2] noot
---
go.mod | 1 +
go.sum | 4 +
src/components/commands/radio.go | 70 ++++++------
src/listenbrainz/listenbrainz.go | 180 +++++++++++++++++++++++++++++++
src/listenbrainz/radio.go | 79 ++++++++++++++
5 files changed, 296 insertions(+), 38 deletions(-)
create mode 100644 src/listenbrainz/listenbrainz.go
create mode 100644 src/listenbrainz/radio.go
diff --git a/go.mod b/go.mod
index 16cf623..425b6b9 100644
--- a/go.mod
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
index cd4fc5d..95af9ab 100644
--- a/go.sum
+++ b/go.sum
@@ -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=
diff --git a/src/components/commands/radio.go b/src/components/commands/radio.go
index 6c90d4e..5361104 100644
--- a/src/components/commands/radio.go
+++ b/src/components/commands/radio.go
@@ -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,25 +218,9 @@ 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,
- })
+ err = c.PlayRadio(radioPlaylist, int(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: pos,
- })
- if err != nil {
- return err
- }
- }
+ return err
}
err = c.Client().Repeat(c.Context, "context")
if err != nil {
@@ -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,23 +589,9 @@ 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,
- })
+ err = c.PlayRadio(radioPlaylist, 0)
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,
- })
- if err != nil {
- return err
- }
- }
+ return err
}
for i := 0; i < 4; i++ {
id := rand.Intn(len(recomendationIds)-2) + 1
diff --git a/src/listenbrainz/listenbrainz.go b/src/listenbrainz/listenbrainz.go
new file mode 100644
index 0000000..ec6acf7
--- /dev/null
+++ b/src/listenbrainz/listenbrainz.go
@@ -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)
+}
diff --git a/src/listenbrainz/radio.go b/src/listenbrainz/radio.go
new file mode 100644
index 0000000..e977ef2
--- /dev/null
+++ b/src/listenbrainz/radio.go
@@ -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"`
+}
--
2.45.2
From a33d0135252418f723362c1e56cae23a05153272 Mon Sep 17 00:00:00 2001
From: a
Date: Mon, 9 Dec 2024 16:14:20 -0600
Subject: [PATCH 2/2] noot
---
main.go | 6 +
src/components/cli/cli.go | 41 ++-
src/components/commands/commander.go | 8 +
src/components/commands/play.go | 2 +-
src/components/commands/radio.go | 507 +++------------------------
src/components/sqldb/component.go | 16 +
src/components/tui/main.go | 204 ++++++-----
src/config/config.go | 15 +-
src/config/logger.go | 22 ++
src/listenbrainz/listenbrainz.go | 51 ++-
10 files changed, 288 insertions(+), 584 deletions(-)
create mode 100644 src/components/sqldb/component.go
create mode 100644 src/config/logger.go
diff --git a/main.go b/main.go
index 04fdecd..f94d713 100644
--- a/main.go
+++ b/main.go
@@ -13,6 +13,9 @@ import (
"git.asdf.cafe/abs3nt/gspot/src/components/cli"
"git.asdf.cafe/abs3nt/gspot/src/components/commands"
"git.asdf.cafe/abs3nt/gspot/src/components/logger"
+ "git.asdf.cafe/abs3nt/gspot/src/components/sqldb"
+ "git.asdf.cafe/abs3nt/gspot/src/config"
+ "git.asdf.cafe/abs3nt/gspot/src/listenbrainz"
"git.asdf.cafe/abs3nt/gspot/src/services"
)
@@ -30,6 +33,8 @@ func main() {
Context,
cache.NewCache,
commands.NewCommander,
+ sqldb.New,
+ listenbrainz.New,
logger.NewLogger,
),
fx.Invoke(
@@ -52,6 +57,7 @@ func Context(
log = slog.Default()
}
ctx, cn := context.WithCancelCause(context.Background())
+ ctx = config.WithLogger(ctx, log)
lc.Append(fx.Hook{
OnStop: func(ctx context.Context) error {
cn(fmt.Errorf("%w: %w", context.Canceled, ErrContextShutdown))
diff --git a/src/components/cli/cli.go b/src/components/cli/cli.go
index 8c5f52d..c919d33 100644
--- a/src/components/cli/cli.go
+++ b/src/components/cli/cli.go
@@ -285,14 +285,37 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
Category: "Info",
},
{
- Name: "radio",
- Usage: "Starts a radio from the current song",
+ Name: "radio",
+ Usage: "creates a radio using a prompt",
+ Flags: []cli.Flag{
+ &cli.StringFlag{
+ Name: "prompt",
+ Aliases: []string{"p"},
+ Usage: "prompt",
+ },
+ &cli.StringFlag{
+ Name: "mode",
+ Aliases: []string{"m"},
+ Value: "hard",
+ Validator: func(s string) error {
+ if s != "hard" && s != "easy" && s != "medium" {
+ return fmt.Errorf("invalid mode: %s (valid: hard, easy, medium)", s)
+ }
+ return nil
+ },
+ Usage: "prompt",
+ },
+ },
Aliases: []string{"r"},
Action: func(ctx context.Context, cmd *cli.Command) error {
if cmd.Args().Present() {
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
}
- return c.Radio()
+ // prompt is the only one we suypport
+ if prompt := cmd.String("prompt"); prompt != "" {
+ return c.RadioFromPrompt(ctx, prompt, cmd.String("mode"))
+ }
+ return fmt.Errorf("radio requires a prompt")
},
Category: "Radio",
},
@@ -308,18 +331,6 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
},
Category: "Radio",
},
- {
- Name: "refillradio",
- Usage: "Refills the radio queue with similar songs",
- Aliases: []string{"rr"},
- Action: func(ctx context.Context, cmd *cli.Command) error {
- if cmd.Args().Present() {
- return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
- }
- return c.RefillRadio()
- },
- Category: "Radio",
- },
{
Name: "status",
Usage: "Prints the current status",
diff --git a/src/components/commands/commander.go b/src/components/commands/commander.go
index 47218be..fc7e01a 100644
--- a/src/components/commands/commander.go
+++ b/src/components/commands/commander.go
@@ -2,6 +2,7 @@ package commands
import (
"context"
+ "database/sql"
"log/slog"
"sync"
@@ -10,6 +11,7 @@ import (
"git.asdf.cafe/abs3nt/gspot/src/components/cache"
"git.asdf.cafe/abs3nt/gspot/src/config"
+ "git.asdf.cafe/abs3nt/gspot/src/listenbrainz"
"git.asdf.cafe/abs3nt/gspot/src/services"
)
@@ -26,6 +28,8 @@ type CommanderParams struct {
Log *slog.Logger
Cache *cache.Cache
Config *config.Config
+ Lb *listenbrainz.ListenBrainz
+ Db *sql.DB
}
type Commander struct {
@@ -36,6 +40,8 @@ type Commander struct {
mu sync.RWMutex
cl *spotify.Client
conf *config.Config
+ lb *listenbrainz.ListenBrainz
+ db *sql.DB
}
func NewCommander(p CommanderParams) CommanderResult {
@@ -44,6 +50,8 @@ func NewCommander(p CommanderParams) CommanderResult {
Log: p.Log,
Cache: p.Cache,
conf: p.Config,
+ lb: p.Lb,
+ db: p.Db,
}
return CommanderResult{
Commander: c,
diff --git a/src/components/commands/play.go b/src/components/commands/play.go
index c084903..4d68cb8 100644
--- a/src/components/commands/play.go
+++ b/src/components/commands/play.go
@@ -35,7 +35,7 @@ func (c *Commander) PlayLikedSongs(position int) error {
if err != nil {
return err
}
- playlist, _, err := c.GetRadioPlaylist("Saved Songs")
+ playlist, err := c.GetRadioPlaylist("Saved Songs")
if err != nil {
return err
}
diff --git a/src/components/commands/radio.go b/src/components/commands/radio.go
index 5361104..06c0c9d 100644
--- a/src/components/commands/radio.go
+++ b/src/components/commands/radio.go
@@ -1,260 +1,60 @@
package commands
import (
+ "context"
"database/sql"
"encoding/json"
"errors"
"fmt"
- "math"
"math/rand"
"os"
"path/filepath"
- "time"
+ "git.asdf.cafe/abs3nt/gspot/src/listenbrainz"
"github.com/zmb3/spotify/v2"
_ "modernc.org/sqlite"
)
func (c *Commander) Radio() error {
- currentSong, err := c.Client().PlayerCurrentlyPlaying(c.Context)
- if err != nil {
- 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 fmt.Errorf("failed to activate device: %w", err)
- }
- tracks, err := c.Client().CurrentUsersTracks(c.Context, spotify.Limit(10))
- if err != nil {
- return fmt.Errorf("failed to get current users tracks: %w", err)
- }
- return c.RadioGivenSong(tracks.Tracks[rand.Intn(len(tracks.Tracks))].SimpleTrack, 0)
-}
-
-func (c *Commander) RadioFromPlaylist(playlist spotify.SimplePlaylist) error {
- playlistPage, err := c.Client().GetPlaylistItems(
- c.Context,
- playlist.ID,
- spotify.Limit(50),
- spotify.Offset(0),
- )
- if err != nil {
- return err
- }
- pageSongs := playlistPage.Items
- rand.Shuffle(len(pageSongs), func(i, j int) { pageSongs[i], pageSongs[j] = pageSongs[j], pageSongs[i] })
- seedCount := 5
- if len(pageSongs) < seedCount {
- seedCount = len(pageSongs)
- }
- seedIds := []spotify.ID{}
- for idx, song := range pageSongs {
- if idx >= seedCount {
- break
- }
- seedIds = append(seedIds, song.Track.Track.ID)
- }
- return c.RadioGivenList(seedIds[:seedCount], playlist.Name)
-}
-
-func (c *Commander) RadioFromSavedTracks() error {
- savedSongs, err := c.Client().CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset(0))
- if err != nil {
- return err
- }
- if savedSongs.Total == 0 {
- return fmt.Errorf("you have no saved songs")
- }
- pages := int(math.Ceil(float64(savedSongs.Total) / 50))
- randomPage := 1
- if pages > 1 {
- randomPage = rand.Intn(pages-1) + 1
- }
- trackPage, err := c.Client().CurrentUsersTracks(c.Context, spotify.Limit(50), spotify.Offset(randomPage*50))
- if err != nil {
- return err
- }
- pageSongs := trackPage.Tracks
- rand.Shuffle(len(pageSongs), func(i, j int) { pageSongs[i], pageSongs[j] = pageSongs[j], pageSongs[i] })
- seedCount := 4
- seedIds := []spotify.ID{}
- for idx, song := range pageSongs {
- if idx >= seedCount {
- break
- }
- seedIds = append(seedIds, song.ID)
- }
- seedIds = append(seedIds, savedSongs.Tracks[0].ID)
- return c.RadioGivenList(seedIds, "Saved Tracks")
-}
-
-func (c *Commander) RadioGivenArtist(artist spotify.SimpleArtist) error {
- seed := spotify.Seeds{
- Artists: []spotify.ID{artist.ID},
- }
- recomendations, err := c.Client().GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100))
- if err != nil {
- return err
- }
- recomendationIds := []spotify.ID{}
- for _, song := range recomendations.Tracks {
- recomendationIds = append(recomendationIds, song.ID)
- }
- err = c.ClearRadio()
- if err != nil {
- return err
- }
- radioPlaylist, db, err := c.GetRadioPlaylist(artist.Name)
- if err != nil {
- return err
- }
- queue := []spotify.ID{}
- for _, rec := range recomendationIds {
- exists, err := c.SongExists(db, rec)
- if err != nil {
- return err
- }
- if !exists {
- _, err := db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", string(rec)))
- if err != nil {
- return err
- }
- queue = append(queue, rec)
- }
- }
- _, err = c.Client().AddTracksToPlaylist(c.Context, radioPlaylist.ID, queue...)
- if err != nil {
- return err
- }
- err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
- PlaybackContext: &radioPlaylist.URI,
- })
- if err != nil {
- return err
- }
- err = c.Client().Repeat(c.Context, "context")
- if err != nil {
- return err
- }
- for i := 0; i < 4; i++ {
- id := rand.Intn(len(recomendationIds)-2) + 1
- seed := spotify.Seeds{
- Tracks: []spotify.ID{recomendationIds[id]},
- }
- additionalRecs, err := c.Client().
- GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100))
- if err != nil {
- return err
- }
- additionalRecsIds := []spotify.ID{}
- for _, song := range additionalRecs.Tracks {
- exists, err := c.SongExists(db, song.ID)
- if err != nil {
- return err
- }
- if !exists {
- _, err = db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", string(song.ID)))
- if err != nil {
- return err
- }
- additionalRecsIds = append(additionalRecsIds, song.ID)
- }
- }
- _, err = c.Client().AddTracksToPlaylist(c.Context, radioPlaylist.ID, additionalRecsIds...)
- if err != nil {
- return err
- }
- }
return nil
}
-func (c *Commander) RadioGivenSong(song spotify.SimpleTrack, pos spotify.Numeric) error {
- start := time.Now().UnixMilli()
- seed := spotify.Seeds{
- Tracks: []spotify.ID{song.ID},
- }
- recomendations, err := c.Client().GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(99))
+func (c *Commander) GetRecomendationIdsForPrompt(ctx context.Context, prompt string, mode string) ([]spotify.ID, error) {
+ radioResp, err := c.lb.RequestRadio(ctx, &listenbrainz.RadioRequest{
+ Prompt: prompt,
+ Mode: mode,
+ })
if err != nil {
- return fmt.Errorf("failed to get recommendations: %w", err)
+ return nil, err
}
- recomendationIds := []spotify.ID{}
- for _, song := range recomendations.Tracks {
- recomendationIds = append(recomendationIds, song.ID)
- }
- err = c.ClearRadio()
- if err != nil {
- return err
- }
- radioPlaylist, db, err := c.GetRadioPlaylist(song.Name)
- if err != nil {
- return err
- }
- _, err = db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", string(song.ID)))
- if err != nil {
- return err
- }
- queue := []spotify.ID{song.ID}
- for _, rec := range recomendationIds {
- exists, err := c.SongExists(db, rec)
+ ids := []spotify.ID{}
+ for _, v := range radioResp.Tracks {
+ match, err := c.lb.MatchTrack(ctx, &v)
if err != nil {
- return err
- }
- if !exists {
- _, err := db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", string(rec)))
- if err != nil {
- return err
+ if errors.Is(err, listenbrainz.ErrNoMatch) {
+ c.Log.Warn("no match for track", "err", err)
+ } else {
+ c.Log.Error("failed to match track", "err", err)
}
- queue = append(queue, rec)
+ continue
}
+ if match.SpotifyId == "" {
+ continue
+ }
+ ids = append(ids, spotify.ID(match.SpotifyId))
}
- _, err = c.Client().AddTracksToPlaylist(c.Context, radioPlaylist.ID, queue...)
+ return ids, nil
+}
+
+func (c *Commander) RadioFromPrompt(ctx context.Context, prompt string, mode string) error {
+ list, err := c.GetRecomendationIdsForPrompt(ctx, prompt, mode)
if err != nil {
return err
}
- delay := time.Now().UnixMilli() - start
- if pos != 0 {
- pos = pos + spotify.Numeric(delay)
- }
- err = c.PlayRadio(radioPlaylist, int(pos))
+ err = c.RadioGivenList(list, fmt.Sprintf("lb - %s - %s", prompt, mode))
if err != nil {
return err
}
- err = c.Client().Repeat(c.Context, "context")
- if err != nil {
- return err
- }
- for i := 0; i < 4; i++ {
- id := rand.Intn(len(recomendationIds)-2) + 1
- seed := spotify.Seeds{
- Tracks: []spotify.ID{recomendationIds[id]},
- }
- additionalRecs, err := c.Client().
- GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100))
- if err != nil {
- return err
- }
- additionalRecsIds := []spotify.ID{}
- for _, song := range additionalRecs.Tracks {
- exists, err := c.SongExists(db, song.ID)
- if err != nil {
- return err
- }
- if !exists {
- _, err = db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", string(song.ID)))
- if err != nil {
- return err
- }
- additionalRecsIds = append(additionalRecsIds, song.ID)
- }
- }
- _, err = c.Client().AddTracksToPlaylist(c.Context, radioPlaylist.ID, additionalRecsIds...)
- if err != nil {
- return err
- }
- }
return nil
}
@@ -283,7 +83,7 @@ func (c *Commander) PlayRadio(radioPlaylist *spotify.FullPlaylist, pos int) erro
}
func (c *Commander) ClearRadio() error {
- radioPlaylist, db, err := c.GetRadioPlaylist("")
+ radioPlaylist, err := c.GetRadioPlaylist("")
if err != nil {
return err
}
@@ -291,57 +91,49 @@ func (c *Commander) ClearRadio() error {
if err != nil {
return err
}
- _, _ = db.Query("DROP TABLE IF EXISTS radio")
+ _, _ = c.db.Query("DROP TABLE IF EXISTS radio")
configDir, _ := os.UserConfigDir()
os.Remove(filepath.Join(configDir, "gspot/radio.json"))
_ = c.Client().Pause(c.Context)
return nil
}
-func (c *Commander) GetRadioPlaylist(name string) (*spotify.FullPlaylist, *sql.DB, error) {
+func (c *Commander) GetRadioPlaylist(name string) (*spotify.FullPlaylist, error) {
configDir, _ := os.UserConfigDir()
playlistFile, err := os.ReadFile(filepath.Join(configDir, "gspot/radio.json"))
if errors.Is(err, os.ErrNotExist) {
return c.CreateRadioPlaylist(name)
}
if err != nil {
- return nil, nil, err
+ return nil, err
}
var playlist *spotify.FullPlaylist
err = json.Unmarshal(playlistFile, &playlist)
if err != nil {
- return nil, nil, err
+ return nil, err
}
- db, err := sql.Open("sqlite", filepath.Join(configDir, "gspot/radio.db"))
- if err != nil {
- return nil, nil, err
- }
- return playlist, db, nil
+ return playlist, nil
}
-func (c *Commander) CreateRadioPlaylist(name string) (*spotify.FullPlaylist, *sql.DB, error) {
+func (c *Commander) CreateRadioPlaylist(name string) (*spotify.FullPlaylist, error) {
// private flag doesnt work
configDir, _ := os.UserConfigDir()
playlist, err := c.Client().
CreatePlaylistForUser(c.Context, c.User.ID, name+" - autoradio", "Automanaged radio playlist", false, false)
if err != nil {
- return nil, nil, err
+ return nil, err
}
raw, err := json.MarshalIndent(playlist, "", " ")
if err != nil {
- return nil, nil, err
+ return nil, err
}
err = os.WriteFile(filepath.Join(configDir, "gspot/radio.json"), raw, 0o600)
if err != nil {
- return nil, nil, err
+ return nil, err
}
- db, err := sql.Open("sqlite", filepath.Join(configDir, "gspot/radio.db"))
- if err != nil {
- return nil, nil, err
- }
- _, _ = db.QueryContext(c.Context, "DROP TABLE IF EXISTS radio")
- _, _ = db.QueryContext(c.Context, "CREATE TABLE IF NOT EXISTS radio (id string PRIMARY KEY)")
- return playlist, db, nil
+ _, _ = c.db.QueryContext(c.Context, "DROP TABLE IF EXISTS radio")
+ _, _ = c.db.QueryContext(c.Context, "CREATE TABLE IF NOT EXISTS radio (id string PRIMARY KEY)")
+ return playlist, nil
}
func (c *Commander) SongExists(db *sql.DB, song spotify.ID) (bool, error) {
@@ -359,226 +151,23 @@ func (c *Commander) SongExists(db *sql.DB, song spotify.ID) (bool, error) {
return true, nil
}
-func (c *Commander) RefillRadio() error {
- status, err := c.Client().PlayerCurrentlyPlaying(c.Context)
- if err != nil {
- return err
- }
- paused := false
- if !status.Playing {
- paused = true
- }
- toRemove := []spotify.ID{}
- radioPlaylist, db, err := c.GetRadioPlaylist("")
- if err != nil {
- return err
- }
-
- playlistItems, err := c.Client().GetPlaylistItems(c.Context, radioPlaylist.ID)
- if err != nil {
- return fmt.Errorf("orig playlist items: %w", err)
- }
-
- if status.PlaybackContext.URI != radioPlaylist.URI || paused {
- return c.RadioFromPlaylist(radioPlaylist.SimplePlaylist)
- }
-
- page := 0
- for {
- tracks, err := c.Client().GetPlaylistItems(c.Context, radioPlaylist.ID, spotify.Limit(50), spotify.Offset(page*50))
- if err != nil {
- return fmt.Errorf("tracks: %w", err)
- }
- if len(tracks.Items) == 0 {
- break
- }
- for _, track := range tracks.Items {
- if track.Track.Track.ID == status.Item.ID {
- break
- }
- toRemove = append(toRemove, track.Track.Track.ID)
- }
- page++
- }
- if len(toRemove) > 0 {
- var trackGroups []spotify.ID
- for idx, item := range toRemove {
- if idx%100 == 0 && idx != 0 {
- _, err = c.Client().RemoveTracksFromPlaylist(c.Context, radioPlaylist.ID, trackGroups...)
- trackGroups = []spotify.ID{}
- }
- trackGroups = append(trackGroups, item)
- if err != nil {
- return fmt.Errorf("error clearing playlist: %w", err)
- }
- }
- _, err := c.Client().RemoveTracksFromPlaylist(c.Context, radioPlaylist.ID, trackGroups...)
- if err != nil {
- return err
- }
- }
-
- toAdd := 500 - (int(playlistItems.Total) - len(toRemove))
- playlistItems, err = c.Client().GetPlaylistItems(c.Context, radioPlaylist.ID)
- if err != nil {
- return fmt.Errorf("playlist items: %w", err)
- }
- total := playlistItems.Total
- pages := int(math.Ceil(float64(total) / 50))
- randomPage := 1
- if pages > 1 {
- randomPage = rand.Intn(pages-1) + 1
- }
- playlistPage, err := c.Client().
- GetPlaylistItems(c.Context, radioPlaylist.ID, spotify.Limit(50), spotify.Offset((randomPage-1)*50))
- if err != nil {
- return fmt.Errorf("playlist page: %w", err)
- }
- pageSongs := playlistPage.Items
- rand.Shuffle(len(pageSongs), func(i, j int) { pageSongs[i], pageSongs[j] = pageSongs[j], pageSongs[i] })
- seedCount := 5
- if len(pageSongs) < seedCount {
- seedCount = len(pageSongs)
- }
- seedIds := []spotify.ID{}
- for idx, song := range pageSongs {
- if idx >= seedCount {
- break
- }
- seedIds = append(seedIds, song.Track.Track.ID)
- }
- seed := spotify.Seeds{
- Tracks: seedIds,
- }
- recomendations, err := c.Client().GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(95))
- if err != nil {
- return err
- }
- recomendationIds := []spotify.ID{}
- for _, song := range recomendations.Tracks {
- exists, err := c.SongExists(db, song.ID)
- if err != nil {
- return fmt.Errorf("err check song existnce: %w", err)
- }
- if !exists {
- recomendationIds = append(recomendationIds, song.ID)
- }
- }
- queue := []spotify.ID{}
- for idx, rec := range recomendationIds {
- if idx > toAdd {
- break
- }
- _, err = db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", rec.String()))
- if err != nil {
- return err
- }
- queue = append(queue, rec)
- }
- toAdd -= len(queue)
- _, err = c.Client().AddTracksToPlaylist(c.Context, radioPlaylist.ID, queue...)
- if err != nil {
- return fmt.Errorf("add tracks: %w", err)
- }
- err = c.Client().Repeat(c.Context, "context")
- if err != nil {
- return fmt.Errorf("repeat: %w", err)
- }
- for toAdd > 0 {
- id := rand.Intn(len(recomendationIds)-2) + 1
- seed := spotify.Seeds{
- Tracks: []spotify.ID{recomendationIds[id]},
- }
- additionalRecs, err := c.Client().
- GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100))
- if err != nil {
- return fmt.Errorf("get recs: %w", err)
- }
- additionalRecsIds := []spotify.ID{}
- for idx, song := range additionalRecs.Tracks {
- exists, err := c.SongExists(db, song.ID)
- if err != nil {
- return fmt.Errorf("check song existence: %w", err)
- }
- if !exists {
- if idx > toAdd {
- break
- }
- additionalRecsIds = append(additionalRecsIds, song.ID)
- queue = append(queue, song.ID)
- }
- }
- toAdd -= len(queue)
- _, err = c.Client().AddTracksToPlaylist(c.Context, radioPlaylist.ID, additionalRecsIds...)
- if err != nil {
- return fmt.Errorf("add tracks to playlist: %w", err)
- }
- }
- return nil
-}
-
-func (c *Commander) RadioFromAlbum(album spotify.SimpleAlbum) error {
- tracks, err := c.AlbumTracks(album.ID, 1)
- if err != nil {
- return err
- }
- total := tracks.Total
- if total == 0 {
- return fmt.Errorf("this playlist is empty")
- }
- pages := int(math.Ceil(float64(total) / 50))
- randomPage := 1
- if pages > 1 {
- randomPage = rand.Intn(pages-1) + 1
- }
- albumTrackPage, err := c.AlbumTracks(album.ID, randomPage)
- if err != nil {
- return err
- }
- pageSongs := albumTrackPage.Tracks
- rand.Shuffle(len(pageSongs), func(i, j int) { pageSongs[i], pageSongs[j] = pageSongs[j], pageSongs[i] })
- seedCount := 5
- if len(pageSongs) < seedCount {
- seedCount = len(pageSongs)
- }
- seedIds := []spotify.ID{}
- for idx, song := range pageSongs {
- if idx >= seedCount {
- break
- }
- seedIds = append(seedIds, song.ID)
- }
- return c.RadioGivenList(seedIds[:seedCount], album.Name)
-}
-
func (c *Commander) RadioGivenList(songs []spotify.ID, name string) error {
- seed := spotify.Seeds{
- Tracks: songs,
- }
- recomendations, err := c.Client().GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(99))
+ err := c.ClearRadio()
if err != nil {
return err
}
- recomendationIds := []spotify.ID{}
- for _, song := range recomendations.Tracks {
- recomendationIds = append(recomendationIds, song.ID)
- }
- err = c.ClearRadio()
- if err != nil {
- return err
- }
- radioPlaylist, db, err := c.GetRadioPlaylist(name)
+ radioPlaylist, err := c.GetRadioPlaylist(name)
if err != nil {
return err
}
queue := []spotify.ID{songs[0]}
- for _, rec := range recomendationIds {
- exists, err := c.SongExists(db, rec)
+ for _, rec := range songs {
+ exists, err := c.SongExists(c.db, rec)
if err != nil {
return err
}
if !exists {
- _, err := db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", string(rec)))
+ _, err := c.db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", string(rec)))
if err != nil {
return err
}
@@ -594,9 +183,9 @@ func (c *Commander) RadioGivenList(songs []spotify.ID, name string) error {
return err
}
for i := 0; i < 4; i++ {
- id := rand.Intn(len(recomendationIds)-2) + 1
+ id := rand.Intn(len(songs)-2) + 1
seed := spotify.Seeds{
- Tracks: []spotify.ID{recomendationIds[id]},
+ Tracks: []spotify.ID{songs[id]},
}
additionalRecs, err := c.Client().GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100))
if err != nil {
@@ -604,12 +193,12 @@ func (c *Commander) RadioGivenList(songs []spotify.ID, name string) error {
}
additionalRecsIds := []spotify.ID{}
for _, song := range additionalRecs.Tracks {
- exists, err := c.SongExists(db, song.ID)
+ exists, err := c.SongExists(c.db, song.ID)
if err != nil {
return err
}
if !exists {
- _, err = db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", string(song.ID)))
+ _, err = c.db.QueryContext(c.Context, fmt.Sprintf("INSERT INTO radio (id) VALUES('%s')", string(song.ID)))
if err != nil {
return err
}
diff --git a/src/components/sqldb/component.go b/src/components/sqldb/component.go
new file mode 100644
index 0000000..9466d80
--- /dev/null
+++ b/src/components/sqldb/component.go
@@ -0,0 +1,16 @@
+package sqldb
+
+import (
+ "database/sql"
+ "os"
+ "path/filepath"
+)
+
+func New() (*sql.DB, error) {
+ configDir, _ := os.UserConfigDir()
+ db, err := sql.Open("sqlite", filepath.Join(configDir, "gspot/radio.db"))
+ if err != nil {
+ return nil, err
+ }
+ return db, nil
+}
diff --git a/src/components/tui/main.go b/src/components/tui/main.go
index b8431e7..6e171d1 100644
--- a/src/components/tui/main.go
+++ b/src/components/tui/main.go
@@ -90,97 +90,119 @@ type mainModel struct {
func (m *mainModel) PlayRadio() {
go m.SendMessage("Starting radio for "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
- selectedItem := m.list.SelectedItem().(mainItem).SpotifyItem
- switch item := selectedItem.(type) {
- case spotify.SimplePlaylist:
- go func() {
- err := m.commands.RadioFromPlaylist(item)
- if err != nil {
- m.SendMessage(err.Error(), 5*time.Second)
- }
- }()
- return
- case *spotify.SavedTrackPage:
- go func() {
- err := m.commands.RadioFromSavedTracks()
- if err != nil {
- m.SendMessage(err.Error(), 5*time.Second)
- }
- }()
- return
- case spotify.SimpleAlbum:
- go func() {
- err := m.commands.RadioFromAlbum(item)
- if err != nil {
- m.SendMessage(err.Error(), 5*time.Second)
- }
- }()
- return
- case spotify.FullAlbum:
- go func() {
- err := m.commands.RadioFromAlbum(item.SimpleAlbum)
- if err != nil {
- m.SendMessage(err.Error(), 5*time.Second)
- }
- }()
- return
- case spotify.SimpleArtist:
- go func() {
- err := m.commands.RadioGivenArtist(item)
- if err != nil {
- m.SendMessage(err.Error(), 5*time.Second)
- }
- }()
- return
- case spotify.FullArtist:
- go func() {
- err := m.commands.RadioGivenArtist(item.SimpleArtist)
- if err != nil {
- m.SendMessage(err.Error(), 5*time.Second)
- }
- }()
- return
- case spotify.SimpleTrack:
- go func() {
- err := m.commands.RadioGivenSong(item, 0)
- if err != nil {
- m.SendMessage(err.Error(), 5*time.Second)
- }
- }()
- return
- case spotify.FullTrack:
- go func() {
- err := m.commands.RadioGivenSong(item.SimpleTrack, 0)
- if err != nil {
- m.SendMessage(err.Error(), 5*time.Second)
- }
- }()
- return
- case spotify.PlaylistTrack:
- go func() {
- err := m.commands.RadioGivenSong(item.Track.SimpleTrack, 0)
- if err != nil {
- m.SendMessage(err.Error(), 5*time.Second)
- }
- }()
- return
- case spotify.PlaylistItem:
- go func() {
- err := m.commands.RadioGivenSong(item.Track.Track.SimpleTrack, 0)
- if err != nil {
- m.SendMessage(err.Error(), 5*time.Second)
- }
- }()
- return
- case spotify.SavedTrack:
- go func() {
- err := m.commands.RadioGivenSong(item.SimpleTrack, 0)
- if err != nil {
- m.SendMessage(err.Error(), 5*time.Second)
- }
- }()
- return
- }
+ // selectedItem := m.list.SelectedItem().(mainItem).SpotifyItem
+ // switch item := selectedItem.(type) {
+ //
+ // case spotify.SimplePlaylist:
+ //
+ // go func() {
+ // err := m.commands.RadioFromPlaylist(item)
+ // if err != nil {
+ // m.SendMessage(err.Error(), 5*time.Second)
+ // }
+ // }()
+ // return
+ //
+ // case *spotify.SavedTrackPage:
+ //
+ // go func() {
+ // err := m.commands.RadioFromSavedTracks()
+ // if err != nil {
+ // m.SendMessage(err.Error(), 5*time.Second)
+ // }
+ // }()
+ // return
+ //
+ // case spotify.SimpleAlbum:
+ //
+ // go func() {
+ // err := m.commands.RadioFromAlbum(item)
+ // if err != nil {
+ // m.SendMessage(err.Error(), 5*time.Second)
+ // }
+ // }()
+ // return
+ //
+ // case spotify.FullAlbum:
+ //
+ // go func() {
+ // err := m.commands.RadioFromAlbum(item.SimpleAlbum)
+ // if err != nil {
+ // m.SendMessage(err.Error(), 5*time.Second)
+ // }
+ // }()
+ // return
+ //
+ // case spotify.SimpleArtist:
+ //
+ // go func() {
+ // err := m.commands.RadioGivenArtist(item)
+ // if err != nil {
+ // m.SendMessage(err.Error(), 5*time.Second)
+ // }
+ // }()
+ // return
+ //
+ // case spotify.FullArtist:
+ //
+ // go func() {
+ // err := m.commands.RadioGivenArtist(item.SimpleArtist)
+ // if err != nil {
+ // m.SendMessage(err.Error(), 5*time.Second)
+ // }
+ // }()
+ // return
+ //
+ // case spotify.SimpleTrack:
+ //
+ // go func() {
+ // err := m.commands.RadioGivenSong(item, 0)
+ // if err != nil {
+ // m.SendMessage(err.Error(), 5*time.Second)
+ // }
+ // }()
+ // return
+ //
+ // case spotify.FullTrack:
+ //
+ // go func() {
+ // err := m.commands.RadioGivenSong(item.SimpleTrack, 0)
+ // if err != nil {
+ // m.SendMessage(err.Error(), 5*time.Second)
+ // }
+ // }()
+ // return
+ //
+ // case spotify.PlaylistTrack:
+ //
+ // go func() {
+ // err := m.commands.RadioGivenSong(item.Track.SimpleTrack, 0)
+ // if err != nil {
+ // m.SendMessage(err.Error(), 5*time.Second)
+ // }
+ // }()
+ // return
+ //
+ // case spotify.PlaylistItem:
+ //
+ // go func() {
+ // err := m.commands.RadioGivenSong(item.Track.Track.SimpleTrack, 0)
+ // if err != nil {
+ // m.SendMessage(err.Error(), 5*time.Second)
+ // }
+ // }()
+ // return
+ //
+ // case spotify.SavedTrack:
+ //
+ // go func() {
+ // err := m.commands.RadioGivenSong(item.SimpleTrack, 0)
+ // if err != nil {
+ // m.SendMessage(err.Error(), 5*time.Second)
+ // }
+ // }()
+ // return
+ // }
}
func (m *mainModel) GoBack() (tea.Cmd, error) {
diff --git a/src/config/config.go b/src/config/config.go
index b7550b9..5f12182 100644
--- a/src/config/config.go
+++ b/src/config/config.go
@@ -1,10 +1,13 @@
package config
type Config struct {
- ClientID string `yaml:"client_id"`
- ClientSecret string `yaml:"client_secret"`
- ClientSecretCmd string `yaml:"client_secret_cmd"`
- Port string `yaml:"port"`
- LogLevel string `yaml:"log_level" default:"info"`
- LogOutput string `yaml:"log_output" default:"stdout"`
+ ListenBrainzEndpoint string `yaml:"listenbrainz_endpoint" default:"https://api.listenbrainz.org"`
+ ListenBrainzLabsEndpoint string `yaml:"listenbrainz_labs_endpoint" default:"https://labs.api.listenbrainz.org"`
+ ListenBrainzUserToken string `yaml:"listenbrainz_user_token"`
+ ClientID string `yaml:"client_id"`
+ ClientSecret string `yaml:"client_secret"`
+ ClientSecretCmd string `yaml:"client_secret_cmd"`
+ Port string `yaml:"port"`
+ LogLevel string `yaml:"log_level" default:"info"`
+ LogOutput string `yaml:"log_output" default:"stdout"`
}
diff --git a/src/config/logger.go b/src/config/logger.go
new file mode 100644
index 0000000..184db6f
--- /dev/null
+++ b/src/config/logger.go
@@ -0,0 +1,22 @@
+package config
+
+import (
+ "context"
+ "log/slog"
+)
+
+type loggerContextKeyType struct{}
+
+var loggerContextKey = loggerContextKeyType{}
+
+func WithLogger(ctx context.Context, logger *slog.Logger) context.Context {
+ return context.WithValue(ctx, loggerContextKey, logger)
+}
+
+func GetLogger(ctx context.Context) *slog.Logger {
+ logger, ok := ctx.Value(loggerContextKey).(*slog.Logger)
+ if !ok {
+ return slog.Default()
+ }
+ return logger
+}
diff --git a/src/listenbrainz/listenbrainz.go b/src/listenbrainz/listenbrainz.go
index ec6acf7..a5e7760 100644
--- a/src/listenbrainz/listenbrainz.go
+++ b/src/listenbrainz/listenbrainz.go
@@ -2,28 +2,32 @@ package listenbrainz
import (
"context"
+ "database/sql"
"fmt"
+ "strings"
+ "git.asdf.cafe/abs3nt/gspot/src/config"
"github.com/go-resty/resty/v2"
)
type ListenBrainz struct {
client *resty.Client
-
- labs *resty.Client
+ labs *resty.Client
+ db *sql.DB
}
-func NewListenBrainz(
- Endpoint string,
- ApiKey string,
+func New(
+ conf *config.Config,
+ db *sql.DB,
) *ListenBrainz {
- c := resty.New().SetBaseURL(Endpoint)
- if ApiKey != "" {
- c = c.SetHeader("Authorization", "Token "+ApiKey)
+ c := resty.New().SetBaseURL(conf.ListenBrainzEndpoint)
+ if conf.ListenBrainzUserToken != "" {
+ c = c.SetHeader("Authorization", "Token "+conf.ListenBrainzUserToken)
}
return &ListenBrainz{
client: c,
labs: resty.New().SetBaseURL("https://labs.api.listenbrainz.org"),
+ db: db,
}
}
@@ -60,12 +64,21 @@ type ApiTrack struct {
Title string `json:"title"`
}
+type ApiError struct {
+ Code int `json:"code"`
+ Error string `json:"error"`
+}
+
func (o *ListenBrainz) RequestRadio(ctx context.Context, req *RadioRequest) (*RadioTracksResponse, error) {
+ log := config.GetLogger(ctx)
+ log.Info("Requesting radio", "prompt", req.Prompt, "mode", req.Mode)
var res RadioApiResponse
+ var errResp ApiError
resp, err := o.client.R().
SetResult(&res).
+ SetError(&errResp).
SetQueryParam("prompt", req.Prompt).
- SetQueryParam("mode", req.Prompt).
+ SetQueryParam("mode", req.Mode).
Get("/1/explore/lb-radio")
if err != nil {
return nil, err
@@ -73,7 +86,7 @@ func (o *ListenBrainz) RequestRadio(ctx context.Context, req *RadioRequest) (*Ra
switch {
case resp.StatusCode() == 200:
default:
- return nil, fmt.Errorf("radio request code %d: %s", resp.StatusCode(), resp.Status())
+ return nil, fmt.Errorf("radio %s: %s", resp.Status(), errResp.Error)
}
tracks := res.Payload.Jspf.Playlist.Track
return &RadioTracksResponse{
@@ -117,21 +130,35 @@ type TrackMatch struct {
var ErrNoMatch = fmt.Errorf("no match")
func (o *ListenBrainz) MatchTrack(ctx context.Context, track *ApiTrack) (*MatchedTrack, error) {
+ return o.matchTrack(ctx, track)
+}
+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 mbid string
+ for _, v := range track.Identifier {
+ if strings.HasPrefix(v, "https://musicbrainz.org/recording/") {
+ mbid = strings.TrimPrefix(v, "https://musicbrainz.org/recording/")
+ break
+ }
+ }
+ if mbid == "" {
+ return nil, fmt.Errorf("%w: no mbid", ErrNoMatch)
+ }
var matches []TrackMatch
// there are mbids, so try to get the first one that is valid
+ var errResp ApiError
resp, err := o.labs.R().
SetResult(&matches).
- SetQueryParam("recording_mbid", track.Identifier[0]).
+ SetQueryParam("recording_mbid", mbid).
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())
+ return nil, fmt.Errorf("spotify-id-from-mbid %s: %s", resp.Status(), errResp.Error)
}
if len(matches) == 0 {
return nil, fmt.Errorf("%w: no mbid", ErrNoMatch)
--
2.45.2