WIP: use the listenbrainz radio api #54
1
go.mod
1
go.mod
@ -8,6 +8,7 @@ require (
|
|||||||
github.com/charmbracelet/bubbles v0.20.0
|
github.com/charmbracelet/bubbles v0.20.0
|
||||||
github.com/charmbracelet/bubbletea v1.1.2
|
github.com/charmbracelet/bubbletea v1.1.2
|
||||||
github.com/charmbracelet/lipgloss v0.13.1
|
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/lmittmann/tint v1.0.5
|
||||||
github.com/rivo/tview v0.0.0-20241016194538-c5e4fb24af13
|
github.com/rivo/tview v0.0.0-20241016194538-c5e4fb24af13
|
||||||
github.com/urfave/cli/v3 v3.0.0-alpha9.1
|
github.com/urfave/cli/v3 v3.0.0-alpha9.1
|
||||||
|
4
go.sum
4
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/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 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
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/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-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/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-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-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.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-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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
6
main.go
6
main.go
@ -13,6 +13,9 @@ import (
|
|||||||
"git.asdf.cafe/abs3nt/gspot/src/components/cli"
|
"git.asdf.cafe/abs3nt/gspot/src/components/cli"
|
||||||
"git.asdf.cafe/abs3nt/gspot/src/components/commands"
|
"git.asdf.cafe/abs3nt/gspot/src/components/commands"
|
||||||
"git.asdf.cafe/abs3nt/gspot/src/components/logger"
|
"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"
|
"git.asdf.cafe/abs3nt/gspot/src/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -30,6 +33,8 @@ func main() {
|
|||||||
Context,
|
Context,
|
||||||
cache.NewCache,
|
cache.NewCache,
|
||||||
commands.NewCommander,
|
commands.NewCommander,
|
||||||
|
sqldb.New,
|
||||||
|
listenbrainz.New,
|
||||||
logger.NewLogger,
|
logger.NewLogger,
|
||||||
),
|
),
|
||||||
fx.Invoke(
|
fx.Invoke(
|
||||||
@ -52,6 +57,7 @@ func Context(
|
|||||||
log = slog.Default()
|
log = slog.Default()
|
||||||
}
|
}
|
||||||
ctx, cn := context.WithCancelCause(context.Background())
|
ctx, cn := context.WithCancelCause(context.Background())
|
||||||
|
ctx = config.WithLogger(ctx, log)
|
||||||
lc.Append(fx.Hook{
|
lc.Append(fx.Hook{
|
||||||
OnStop: func(ctx context.Context) error {
|
OnStop: func(ctx context.Context) error {
|
||||||
cn(fmt.Errorf("%w: %w", context.Canceled, ErrContextShutdown))
|
cn(fmt.Errorf("%w: %w", context.Canceled, ErrContextShutdown))
|
||||||
|
@ -286,13 +286,36 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "radio",
|
Name: "radio",
|
||||||
Usage: "Starts a radio from the current song",
|
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"},
|
Aliases: []string{"r"},
|
||||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
if cmd.Args().Present() {
|
if cmd.Args().Present() {
|
||||||
return fmt.Errorf("unexpected arguments: %s", strings.Join(cmd.Args().Slice(), " "))
|
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",
|
Category: "Radio",
|
||||||
},
|
},
|
||||||
@ -308,18 +331,6 @@ func Run(c *commands.Commander, s fx.Shutdowner) {
|
|||||||
},
|
},
|
||||||
Category: "Radio",
|
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",
|
Name: "status",
|
||||||
Usage: "Prints the current status",
|
Usage: "Prints the current status",
|
||||||
|
@ -2,6 +2,7 @@ package commands
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@ -10,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"git.asdf.cafe/abs3nt/gspot/src/components/cache"
|
"git.asdf.cafe/abs3nt/gspot/src/components/cache"
|
||||||
"git.asdf.cafe/abs3nt/gspot/src/config"
|
"git.asdf.cafe/abs3nt/gspot/src/config"
|
||||||
|
"git.asdf.cafe/abs3nt/gspot/src/listenbrainz"
|
||||||
"git.asdf.cafe/abs3nt/gspot/src/services"
|
"git.asdf.cafe/abs3nt/gspot/src/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -26,6 +28,8 @@ type CommanderParams struct {
|
|||||||
Log *slog.Logger
|
Log *slog.Logger
|
||||||
Cache *cache.Cache
|
Cache *cache.Cache
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
|
Lb *listenbrainz.ListenBrainz
|
||||||
|
Db *sql.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
type Commander struct {
|
type Commander struct {
|
||||||
@ -36,6 +40,8 @@ type Commander struct {
|
|||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
cl *spotify.Client
|
cl *spotify.Client
|
||||||
conf *config.Config
|
conf *config.Config
|
||||||
|
lb *listenbrainz.ListenBrainz
|
||||||
|
db *sql.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCommander(p CommanderParams) CommanderResult {
|
func NewCommander(p CommanderParams) CommanderResult {
|
||||||
@ -44,6 +50,8 @@ func NewCommander(p CommanderParams) CommanderResult {
|
|||||||
Log: p.Log,
|
Log: p.Log,
|
||||||
Cache: p.Cache,
|
Cache: p.Cache,
|
||||||
conf: p.Config,
|
conf: p.Config,
|
||||||
|
lb: p.Lb,
|
||||||
|
db: p.Db,
|
||||||
}
|
}
|
||||||
return CommanderResult{
|
return CommanderResult{
|
||||||
Commander: c,
|
Commander: c,
|
||||||
|
@ -35,7 +35,7 @@ func (c *Commander) PlayLikedSongs(position int) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
playlist, _, err := c.GetRadioPlaylist("Saved Songs")
|
playlist, err := c.GetRadioPlaylist("Saved Songs")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1,226 +1,67 @@
|
|||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
|
||||||
|
|
||||||
|
"git.asdf.cafe/abs3nt/gspot/src/listenbrainz"
|
||||||
"github.com/zmb3/spotify/v2"
|
"github.com/zmb3/spotify/v2"
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Commander) Radio() error {
|
func (c *Commander) Radio() error {
|
||||||
currentSong, err := c.Client().PlayerCurrentlyPlaying(c.Context)
|
return nil
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if currentSong.Item != nil {
|
|
||||||
return c.RadioGivenSong(currentSong.Item.SimpleTrack, currentSong.Progress)
|
|
||||||
}
|
|
||||||
_, err = c.activateDevice()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tracks, err := c.Client().CurrentUsersTracks(c.Context, spotify.Limit(10))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.RadioGivenSong(tracks.Tracks[rand.Intn(len(tracks.Tracks))].SimpleTrack, 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commander) RadioFromPlaylist(playlist spotify.SimplePlaylist) error {
|
func (c *Commander) GetRecomendationIdsForPrompt(ctx context.Context, prompt string, mode string) ([]spotify.ID, error) {
|
||||||
playlistPage, err := c.Client().GetPlaylistItems(
|
radioResp, err := c.lb.RequestRadio(ctx, &listenbrainz.RadioRequest{
|
||||||
c.Context,
|
Prompt: prompt,
|
||||||
playlist.ID,
|
Mode: mode,
|
||||||
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 {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
err = c.Client().Repeat(c.Context, "context")
|
ids := []spotify.ID{}
|
||||||
|
for _, v := range radioResp.Tracks {
|
||||||
|
match, err := c.lb.MatchTrack(ctx, &v)
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if match.SpotifyId == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ids = append(ids, spotify.ID(match.SpotifyId))
|
||||||
|
}
|
||||||
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for i := 0; i < 4; i++ {
|
err = c.RadioGivenList(list, fmt.Sprintf("lb - %s - %s", prompt, mode))
|
||||||
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 {
|
if err != nil {
|
||||||
return err
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commander) RadioGivenSong(song spotify.SimpleTrack, pos spotify.Numeric) error {
|
func (c *Commander) PlayRadio(radioPlaylist *spotify.FullPlaylist, pos int) error {
|
||||||
start := time.Now().UnixMilli()
|
err := c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
|
||||||
seed := spotify.Seeds{
|
|
||||||
Tracks: []spotify.ID{song.ID},
|
|
||||||
}
|
|
||||||
recomendations, err := c.Client().GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(99))
|
|
||||||
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(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)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
delay := time.Now().UnixMilli() - start
|
|
||||||
if pos != 0 {
|
|
||||||
pos = pos + spotify.Numeric(delay)
|
|
||||||
}
|
|
||||||
err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
|
|
||||||
PlaybackContext: &radioPlaylist.URI,
|
PlaybackContext: &radioPlaylist.URI,
|
||||||
PositionMs: pos,
|
PositionMs: spotify.Numeric(pos),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if isNoActiveError(err) {
|
if isNoActiveError(err) {
|
||||||
@ -231,51 +72,18 @@ func (c *Commander) RadioGivenSong(song spotify.SimpleTrack, pos spotify.Numeric
|
|||||||
err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
|
err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
|
||||||
PlaybackContext: &radioPlaylist.URI,
|
PlaybackContext: &radioPlaylist.URI,
|
||||||
DeviceID: &deviceID,
|
DeviceID: &deviceID,
|
||||||
PositionMs: pos,
|
PositionMs: spotify.Numeric(pos),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commander) ClearRadio() error {
|
func (c *Commander) ClearRadio() error {
|
||||||
radioPlaylist, db, err := c.GetRadioPlaylist("")
|
radioPlaylist, err := c.GetRadioPlaylist("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -283,57 +91,49 @@ func (c *Commander) ClearRadio() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, _ = db.Query("DROP TABLE IF EXISTS radio")
|
_, _ = c.db.Query("DROP TABLE IF EXISTS radio")
|
||||||
configDir, _ := os.UserConfigDir()
|
configDir, _ := os.UserConfigDir()
|
||||||
os.Remove(filepath.Join(configDir, "gspot/radio.json"))
|
os.Remove(filepath.Join(configDir, "gspot/radio.json"))
|
||||||
_ = c.Client().Pause(c.Context)
|
_ = c.Client().Pause(c.Context)
|
||||||
return nil
|
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()
|
configDir, _ := os.UserConfigDir()
|
||||||
playlistFile, err := os.ReadFile(filepath.Join(configDir, "gspot/radio.json"))
|
playlistFile, err := os.ReadFile(filepath.Join(configDir, "gspot/radio.json"))
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
return c.CreateRadioPlaylist(name)
|
return c.CreateRadioPlaylist(name)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var playlist *spotify.FullPlaylist
|
var playlist *spotify.FullPlaylist
|
||||||
err = json.Unmarshal(playlistFile, &playlist)
|
err = json.Unmarshal(playlistFile, &playlist)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
db, err := sql.Open("sqlite", filepath.Join(configDir, "gspot/radio.db"))
|
return playlist, nil
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
return playlist, db, 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
|
// private flag doesnt work
|
||||||
configDir, _ := os.UserConfigDir()
|
configDir, _ := os.UserConfigDir()
|
||||||
playlist, err := c.Client().
|
playlist, err := c.Client().
|
||||||
CreatePlaylistForUser(c.Context, c.User.ID, name+" - autoradio", "Automanaged radio playlist", false, false)
|
CreatePlaylistForUser(c.Context, c.User.ID, name+" - autoradio", "Automanaged radio playlist", false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
raw, err := json.MarshalIndent(playlist, "", " ")
|
raw, err := json.MarshalIndent(playlist, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
err = os.WriteFile(filepath.Join(configDir, "gspot/radio.json"), raw, 0o600)
|
err = os.WriteFile(filepath.Join(configDir, "gspot/radio.json"), raw, 0o600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
db, err := sql.Open("sqlite", filepath.Join(configDir, "gspot/radio.db"))
|
_, _ = c.db.QueryContext(c.Context, "DROP TABLE IF EXISTS radio")
|
||||||
if err != nil {
|
_, _ = c.db.QueryContext(c.Context, "CREATE TABLE IF NOT EXISTS radio (id string PRIMARY KEY)")
|
||||||
return nil, nil, err
|
return playlist, nil
|
||||||
}
|
|
||||||
_, _ = 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Commander) SongExists(db *sql.DB, song spotify.ID) (bool, error) {
|
func (c *Commander) SongExists(db *sql.DB, song spotify.ID) (bool, error) {
|
||||||
@ -351,226 +151,23 @@ func (c *Commander) SongExists(db *sql.DB, song spotify.ID) (bool, error) {
|
|||||||
return true, nil
|
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 {
|
func (c *Commander) RadioGivenList(songs []spotify.ID, name string) error {
|
||||||
seed := spotify.Seeds{
|
err := c.ClearRadio()
|
||||||
Tracks: songs,
|
|
||||||
}
|
|
||||||
recomendations, err := c.Client().GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(99))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
recomendationIds := []spotify.ID{}
|
radioPlaylist, err := c.GetRadioPlaylist(name)
|
||||||
for _, song := range recomendations.Tracks {
|
|
||||||
recomendationIds = append(recomendationIds, song.ID)
|
|
||||||
}
|
|
||||||
err = c.ClearRadio()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
radioPlaylist, db, err := c.GetRadioPlaylist(name)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
queue := []spotify.ID{songs[0]}
|
queue := []spotify.ID{songs[0]}
|
||||||
for _, rec := range recomendationIds {
|
for _, rec := range songs {
|
||||||
exists, err := c.SongExists(db, rec)
|
exists, err := c.SongExists(c.db, rec)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !exists {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -581,28 +178,14 @@ func (c *Commander) RadioGivenList(songs []spotify.ID, name string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = c.Client().PlayOpt(c.Context, &spotify.PlayOptions{
|
err = c.PlayRadio(radioPlaylist, 0)
|
||||||
PlaybackContext: &radioPlaylist.URI,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
if isNoActiveError(err) {
|
|
||||||
deviceID, err := c.activateDevice()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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++ {
|
for i := 0; i < 4; i++ {
|
||||||
id := rand.Intn(len(recomendationIds)-2) + 1
|
id := rand.Intn(len(songs)-2) + 1
|
||||||
seed := spotify.Seeds{
|
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))
|
additionalRecs, err := c.Client().GetRecommendations(c.Context, seed, &spotify.TrackAttributes{}, spotify.Limit(100))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -610,12 +193,12 @@ func (c *Commander) RadioGivenList(songs []spotify.ID, name string) error {
|
|||||||
}
|
}
|
||||||
additionalRecsIds := []spotify.ID{}
|
additionalRecsIds := []spotify.ID{}
|
||||||
for _, song := range additionalRecs.Tracks {
|
for _, song := range additionalRecs.Tracks {
|
||||||
exists, err := c.SongExists(db, song.ID)
|
exists, err := c.SongExists(c.db, song.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !exists {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
16
src/components/sqldb/component.go
Normal file
16
src/components/sqldb/component.go
Normal file
@ -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
|
||||||
|
}
|
@ -90,97 +90,119 @@ type mainModel struct {
|
|||||||
|
|
||||||
func (m *mainModel) PlayRadio() {
|
func (m *mainModel) PlayRadio() {
|
||||||
go m.SendMessage("Starting radio for "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
|
go m.SendMessage("Starting radio for "+m.list.SelectedItem().(mainItem).Title(), 2*time.Second)
|
||||||
selectedItem := m.list.SelectedItem().(mainItem).SpotifyItem
|
// selectedItem := m.list.SelectedItem().(mainItem).SpotifyItem
|
||||||
switch item := selectedItem.(type) {
|
// switch item := selectedItem.(type) {
|
||||||
case spotify.SimplePlaylist:
|
//
|
||||||
go func() {
|
// case spotify.SimplePlaylist:
|
||||||
err := m.commands.RadioFromPlaylist(item)
|
//
|
||||||
if err != nil {
|
// go func() {
|
||||||
m.SendMessage(err.Error(), 5*time.Second)
|
// err := m.commands.RadioFromPlaylist(item)
|
||||||
}
|
// if err != nil {
|
||||||
}()
|
// m.SendMessage(err.Error(), 5*time.Second)
|
||||||
return
|
// }
|
||||||
case *spotify.SavedTrackPage:
|
// }()
|
||||||
go func() {
|
// return
|
||||||
err := m.commands.RadioFromSavedTracks()
|
//
|
||||||
if err != nil {
|
// case *spotify.SavedTrackPage:
|
||||||
m.SendMessage(err.Error(), 5*time.Second)
|
//
|
||||||
}
|
// go func() {
|
||||||
}()
|
// err := m.commands.RadioFromSavedTracks()
|
||||||
return
|
// if err != nil {
|
||||||
case spotify.SimpleAlbum:
|
// m.SendMessage(err.Error(), 5*time.Second)
|
||||||
go func() {
|
// }
|
||||||
err := m.commands.RadioFromAlbum(item)
|
// }()
|
||||||
if err != nil {
|
// return
|
||||||
m.SendMessage(err.Error(), 5*time.Second)
|
//
|
||||||
}
|
// case spotify.SimpleAlbum:
|
||||||
}()
|
//
|
||||||
return
|
// go func() {
|
||||||
case spotify.FullAlbum:
|
// err := m.commands.RadioFromAlbum(item)
|
||||||
go func() {
|
// if err != nil {
|
||||||
err := m.commands.RadioFromAlbum(item.SimpleAlbum)
|
// m.SendMessage(err.Error(), 5*time.Second)
|
||||||
if err != nil {
|
// }
|
||||||
m.SendMessage(err.Error(), 5*time.Second)
|
// }()
|
||||||
}
|
// return
|
||||||
}()
|
//
|
||||||
return
|
// case spotify.FullAlbum:
|
||||||
case spotify.SimpleArtist:
|
//
|
||||||
go func() {
|
// go func() {
|
||||||
err := m.commands.RadioGivenArtist(item)
|
// err := m.commands.RadioFromAlbum(item.SimpleAlbum)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
m.SendMessage(err.Error(), 5*time.Second)
|
// m.SendMessage(err.Error(), 5*time.Second)
|
||||||
}
|
// }
|
||||||
}()
|
// }()
|
||||||
return
|
// return
|
||||||
case spotify.FullArtist:
|
//
|
||||||
go func() {
|
// case spotify.SimpleArtist:
|
||||||
err := m.commands.RadioGivenArtist(item.SimpleArtist)
|
//
|
||||||
if err != nil {
|
// go func() {
|
||||||
m.SendMessage(err.Error(), 5*time.Second)
|
// err := m.commands.RadioGivenArtist(item)
|
||||||
}
|
// if err != nil {
|
||||||
}()
|
// m.SendMessage(err.Error(), 5*time.Second)
|
||||||
return
|
// }
|
||||||
case spotify.SimpleTrack:
|
// }()
|
||||||
go func() {
|
// return
|
||||||
err := m.commands.RadioGivenSong(item, 0)
|
//
|
||||||
if err != nil {
|
// case spotify.FullArtist:
|
||||||
m.SendMessage(err.Error(), 5*time.Second)
|
//
|
||||||
}
|
// go func() {
|
||||||
}()
|
// err := m.commands.RadioGivenArtist(item.SimpleArtist)
|
||||||
return
|
// if err != nil {
|
||||||
case spotify.FullTrack:
|
// m.SendMessage(err.Error(), 5*time.Second)
|
||||||
go func() {
|
// }
|
||||||
err := m.commands.RadioGivenSong(item.SimpleTrack, 0)
|
// }()
|
||||||
if err != nil {
|
// return
|
||||||
m.SendMessage(err.Error(), 5*time.Second)
|
//
|
||||||
}
|
// case spotify.SimpleTrack:
|
||||||
}()
|
//
|
||||||
return
|
// go func() {
|
||||||
case spotify.PlaylistTrack:
|
// err := m.commands.RadioGivenSong(item, 0)
|
||||||
go func() {
|
// if err != nil {
|
||||||
err := m.commands.RadioGivenSong(item.Track.SimpleTrack, 0)
|
// m.SendMessage(err.Error(), 5*time.Second)
|
||||||
if err != nil {
|
// }
|
||||||
m.SendMessage(err.Error(), 5*time.Second)
|
// }()
|
||||||
}
|
// return
|
||||||
}()
|
//
|
||||||
return
|
// case spotify.FullTrack:
|
||||||
case spotify.PlaylistItem:
|
//
|
||||||
go func() {
|
// go func() {
|
||||||
err := m.commands.RadioGivenSong(item.Track.Track.SimpleTrack, 0)
|
// err := m.commands.RadioGivenSong(item.SimpleTrack, 0)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
m.SendMessage(err.Error(), 5*time.Second)
|
// m.SendMessage(err.Error(), 5*time.Second)
|
||||||
}
|
// }
|
||||||
}()
|
// }()
|
||||||
return
|
// return
|
||||||
case spotify.SavedTrack:
|
//
|
||||||
go func() {
|
// case spotify.PlaylistTrack:
|
||||||
err := m.commands.RadioGivenSong(item.SimpleTrack, 0)
|
//
|
||||||
if err != nil {
|
// go func() {
|
||||||
m.SendMessage(err.Error(), 5*time.Second)
|
// err := m.commands.RadioGivenSong(item.Track.SimpleTrack, 0)
|
||||||
}
|
// if err != nil {
|
||||||
}()
|
// m.SendMessage(err.Error(), 5*time.Second)
|
||||||
return
|
// }
|
||||||
}
|
// }()
|
||||||
|
// 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) {
|
func (m *mainModel) GoBack() (tea.Cmd, error) {
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
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"`
|
ClientID string `yaml:"client_id"`
|
||||||
ClientSecret string `yaml:"client_secret"`
|
ClientSecret string `yaml:"client_secret"`
|
||||||
ClientSecretCmd string `yaml:"client_secret_cmd"`
|
ClientSecretCmd string `yaml:"client_secret_cmd"`
|
||||||
|
22
src/config/logger.go
Normal file
22
src/config/logger.go
Normal file
@ -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
|
||||||
|
}
|
207
src/listenbrainz/listenbrainz.go
Normal file
207
src/listenbrainz/listenbrainz.go
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
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
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(
|
||||||
|
conf *config.Config,
|
||||||
|
db *sql.DB,
|
||||||
|
) *ListenBrainz {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Mode).
|
||||||
|
Get("/1/explore/lb-radio")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case resp.StatusCode() == 200:
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("radio %s: %s", resp.Status(), errResp.Error)
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
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", mbid).
|
||||||
|
Get("/spotify-id-from-mbid/json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if resp.StatusCode() != 200 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
// 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
79
src/listenbrainz/radio.go
Normal 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"`
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user