diff --git a/go.mod b/go.mod index a8ed6bd..499d6bb 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/charmbracelet/lipgloss v0.6.0 github.com/cristalhq/aconfig v0.18.3 github.com/cristalhq/aconfig/aconfigyaml v0.17.1 + github.com/zmb3/spotify v1.3.0 github.com/zmb3/spotify/v2 v2.3.1 golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5 ) diff --git a/go.sum b/go.sum index 4775f2c..12efd20 100644 --- a/go.sum +++ b/go.sum @@ -171,6 +171,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zmb3/spotify v1.3.0 h1:6Z2F1IMx0Hviq/dpf8nFwvKPppFEMXn8yfReSBVi16k= +github.com/zmb3/spotify v1.3.0/go.mod h1:GD7AAEMUJVYc2Z7p2a2S0E3/5f/KxM/vOnErNr4j+Tw= github.com/zmb3/spotify/v2 v2.3.1 h1:aEyIPotROM3JJjHMCImFROgnPIUpzVo8wymYSaPSd9w= github.com/zmb3/spotify/v2 v2.3.1/go.mod h1:+LVh9CafHu7SedyqYmEf12Rd01dIVlEL845yNhksW0E= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= diff --git a/internal/api/api.go b/internal/api/api.go index f7f9f01..7a4314c 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -22,18 +22,24 @@ func Run(ctx *gctx.Context, client *spotify.Client, args []string) error { switch args[0] { case "play": return commands.Play(ctx, client) - case "playurl": - return commands.PlayUrl(ctx, client, args) case "pause": return commands.Pause(ctx, client) + case "toggleplay": + return commands.TogglePlay(ctx, client) + case "next": + return commands.Skip(ctx, client) + case "previous": + return commands.Previous(ctx, client) + case "playurl": + return commands.PlayUrl(ctx, client, args) case "like": return commands.Like(ctx, client) case "unlike": return commands.Unlike(ctx, client) - case "next": - return commands.Skip(ctx, client) case "shuffle": return commands.Shuffle(ctx, client) + case "repeat": + return commands.Repeat(ctx, client) case "radio": return commands.Radio(ctx, client) case "clearradio": @@ -47,7 +53,7 @@ func Run(ctx *gctx.Context, client *spotify.Client, args []string) error { case "devices": return commands.Devices(ctx, client) case "setdevice": - return commands.SetDevice(ctx, client, args) + return tui.DisplayDevices(ctx, client) default: return fmt.Errorf("Unsupported Command") } diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 16e0365..6d27b6d 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -20,9 +20,17 @@ func Play(ctx *gctx.Context, client *spotify.Client) error { err := client.Play(ctx) if err != nil { if isNoActiveError(err) { - return playWithTransfer(ctx, client) + err := activateDevice(ctx, client) + if err != nil { + return err + } + err = client.Play(ctx) + if err != nil { + return err + } + } else { + return err } - return err } ctx.Println("Playing!") return nil @@ -40,7 +48,11 @@ func PlayUrl(ctx *gctx.Context, client *spotify.Client, args []string) error { err = client.QueueSong(ctx, spotify.ID(track_id)) if err != nil { if isNoActiveError(err) { - err = queueWithTransfer(ctx, client, spotify.ID(track_id)) + err := activateDevice(ctx, client) + if err != nil { + return err + } + err = client.QueueSong(ctx, spotify.ID(track_id)) if err != nil { return err } @@ -50,8 +62,9 @@ func PlayUrl(ctx *gctx.Context, client *spotify.Client, args []string) error { } ctx.Println("Playing!") return nil + } else { + return err } - return err } err = client.Next(ctx) if err != nil { @@ -65,14 +78,19 @@ func QueueSong(ctx *gctx.Context, client *spotify.Client, id spotify.ID) error { err := client.QueueSong(ctx, id) if err != nil { if isNoActiveError(err) { - err := queueWithTransfer(ctx, client, id) + err := activateDevice(ctx, client) + if err != nil { + return err + } + err = client.QueueSong(ctx, id) if err != nil { return err } ctx.Println("Queued!") return nil + } else { + return err } - return err } ctx.Println("Queued!") return nil @@ -255,6 +273,17 @@ func Pause(ctx *gctx.Context, client *spotify.Client) error { return nil } +func TogglePlay(ctx *gctx.Context, client *spotify.Client) error { + current, err := client.PlayerCurrentlyPlaying(ctx) + if err != nil { + return err + } + if !current.Playing { + return Play(ctx, client) + } + return Pause(ctx, client) +} + func Like(ctx *gctx.Context, client *spotify.Client) error { playing, err := client.PlayerCurrentlyPlaying(ctx) if err != nil { @@ -290,6 +319,15 @@ func Skip(ctx *gctx.Context, client *spotify.Client) error { return nil } +func Previous(ctx *gctx.Context, client *spotify.Client) error { + err := client.Previous(ctx) + if err != nil { + return err + } + ctx.Println("Previous!") + return nil +} + func Status(ctx *gctx.Context, client *spotify.Client) error { state, err := client.PlayerState(ctx) if err != nil { @@ -311,6 +349,25 @@ func Shuffle(ctx *gctx.Context, client *spotify.Client) error { return nil } +func Repeat(ctx *gctx.Context, client *spotify.Client) error { + state, err := client.PlayerState(ctx) + if err != nil { + return fmt.Errorf("Failed to get current playstate") + } + fmt.Println(state.RepeatState) + newState := "off" + if state.RepeatState == "off" { + newState = "context" + } + // spotifyd only supports binary value for repeat, context or off, change when/if spotifyd is better + err = client.Repeat(ctx, newState) + if err != nil { + return err + } + ctx.Println("Repeat set to", newState) + return nil +} + func TrackList(ctx *gctx.Context, client *spotify.Client, page int) (*spotify.SavedTrackPage, error) { return client.CurrentUsersTracks(ctx, spotify.Limit(50), spotify.Offset((page-1)*50)) } @@ -335,31 +392,21 @@ func PrintDevices(devices []spotify.PlayerDevice) error { return nil } -func SetDevice(ctx *gctx.Context, client *spotify.Client, args []string) error { - if len(args) < 2 { - return fmt.Errorf("Please provide your device ID") - } - devices, err := client.PlayerDevices(ctx) +func SetDevice(ctx *gctx.Context, client *spotify.Client, device spotify.PlayerDevice) error { + out, err := json.MarshalIndent(device, "", " ") if err != nil { return err } - var set_device spotify.PlayerDevice - for _, device := range devices { - if device.ID.String() == args[1] { - set_device = device - break - } - } - out, err := json.MarshalIndent(set_device, "", " ") + configDir, _ := os.UserConfigDir() + err = ioutil.WriteFile(filepath.Join(configDir, "gospt/device.json"), out, 0o644) if err != nil { return err } - homdir, _ := os.UserHomeDir() - err = ioutil.WriteFile(filepath.Join(homdir, ".config/gospt/device.json"), out, 0o644) + err = activateDevice(ctx, client) if err != nil { return err } - fmt.Println("Your device has been set to: ", set_device.Name) + fmt.Println("Your device has been set to: ", device.Name) return nil } @@ -367,77 +414,37 @@ func isNoActiveError(err error) bool { return strings.Contains(err.Error(), "No active device found") } -func playWithTransfer(ctx *gctx.Context, client *spotify.Client) error { - configDir, _ := os.UserConfigDir() - deviceFile, err := os.Open(filepath.Join(configDir, "gospt/device.json")) - if err != nil { - return err - } - defer deviceFile.Close() - deviceValue, err := ioutil.ReadAll(deviceFile) - if err != nil { - return err - } - var device *spotify.PlayerDevice - err = json.Unmarshal(deviceValue, &device) - if err != nil { - return err - } - err = client.TransferPlayback(ctx, device.ID, true) - if err != nil { - return err - } - ctx.Println("Playing!") - return nil -} - -func queueWithTransfer(ctx *gctx.Context, client *spotify.Client, track_id spotify.ID) error { - configDir, _ := os.UserConfigDir() - deviceFile, err := os.Open(filepath.Join(configDir, "gospt/device.json")) - if err != nil { - return err - } - defer deviceFile.Close() - deviceValue, err := ioutil.ReadAll(deviceFile) - if err != nil { - return err - } - var device *spotify.PlayerDevice - err = json.Unmarshal(deviceValue, &device) - if err != nil { - return err - } - err = client.TransferPlayback(ctx, device.ID, true) - if err != nil { - return err - } - err = client.QueueSong(ctx, track_id) - if err != nil { - return err - } - ctx.Println("Playing!") - return nil -} - func activateDevice(ctx *gctx.Context, client *spotify.Client) error { + to_play := true + current, err := client.PlayerCurrentlyPlaying(ctx) + if err != nil { + return err + } + if current.Item == nil || !current.Playing { + to_play = false + } configDir, _ := os.UserConfigDir() - deviceFile, err := os.Open(filepath.Join(configDir, "gospt/device.json")) - if err != nil { - return err - } - defer deviceFile.Close() - deviceValue, err := ioutil.ReadAll(deviceFile) - if err != nil { - return err - } - var device *spotify.PlayerDevice - err = json.Unmarshal(deviceValue, &device) - if err != nil { - return err - } - err = client.TransferPlayback(ctx, device.ID, false) - if err != nil { - return err + if _, err := os.Stat(filepath.Join(configDir, "gospt/device.json")); err == nil { + deviceFile, err := os.Open(filepath.Join(configDir, "gospt/device.json")) + if err != nil { + return err + } + defer deviceFile.Close() + deviceValue, err := ioutil.ReadAll(deviceFile) + if err != nil { + return err + } + var device *spotify.PlayerDevice + err = json.Unmarshal(deviceValue, &device) + if err != nil { + return err + } + err = client.TransferPlayback(ctx, device.ID, to_play) + if err != nil { + return err + } + } else { + fmt.Println("YOU MUST RUN gospt setdevice FIRST") } return nil } diff --git a/internal/tui/device.go b/internal/tui/device.go new file mode 100644 index 0000000..3309703 --- /dev/null +++ b/internal/tui/device.go @@ -0,0 +1,95 @@ +package tui + +import ( + "fmt" + "os" + + "gospt/internal/commands" + "gospt/internal/gctx" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/zmb3/spotify/v2" +) + +var deviceDocStyle = lipgloss.NewStyle().Margin(1, 2) + +type deviceItem struct { + spotify.PlayerDevice +} + +func (i deviceItem) Title() string { return i.Name } +func (i deviceItem) Description() string { + return fmt.Sprintf("%s - active: %t", i.ID, i.Active) +} +func (i deviceItem) FilterValue() string { return i.Title() } + +type deviceModel struct { + list list.Model + page int + ctx *gctx.Context + client *spotify.Client +} + +func (m deviceModel) Init() tea.Cmd { + return nil +} + +func (m deviceModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + if msg.String() == "ctrl+c" { + return m, tea.Quit + } + if msg.String() == "enter" { + fmt.Println("SELECTING") + device := m.list.SelectedItem() + var err error + err = commands.SetDevice(m.ctx, m.client, device.(deviceItem).PlayerDevice) + if err != nil { + m.ctx.Printf(err.Error()) + } + fmt.Println("DEVICE SET") + return m, tea.Quit + } + case tea.WindowSizeMsg: + h, v := docStyle.GetFrameSize() + m.list.SetSize(msg.Width-h, msg.Height-v) + } + + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd +} + +func (m deviceModel) View() string { + return docStyle.Render(m.list.View()) +} + +func DisplayDevices(ctx *gctx.Context, client *spotify.Client) error { + items := []list.Item{} + devices, err := client.PlayerDevices(ctx) + if err != nil { + return err + } + for _, device := range devices { + items = append(items, deviceItem{ + device, + }) + } + if err != nil { + return err + } + m := deviceModel{list: list.New(items, list.NewDefaultDelegate(), 0, 0), page: 1, ctx: ctx, client: client} + m.list.Title = "Saved Tracks" + + p := tea.NewProgram(m, tea.WithAltScreen()) + + if _, err := p.Run(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } + fmt.Println("DEVICE SET AND SAVED") + return nil +}