diff --git a/cmd/playlists.go b/cmd/playlists.go deleted file mode 100644 index 371407a..0000000 --- a/cmd/playlists.go +++ /dev/null @@ -1,20 +0,0 @@ -package cmd - -import ( - "gospt/internal/tui" - - "github.com/spf13/cobra" -) - -func init() { - rootCmd.AddCommand(playListsCmd) -} - -var playListsCmd = &cobra.Command{ - Use: "playlists", - Short: "Uses tui to show users playlists", - Long: `Opens tui showing all users playlists`, - Run: func(cmd *cobra.Command, args []string) { - tui.DisplayPlaylists(ctx, client) - }, -} diff --git a/cmd/setdevice.go b/cmd/setdevice.go index 0f23087..d2330ce 100644 --- a/cmd/setdevice.go +++ b/cmd/setdevice.go @@ -15,6 +15,6 @@ var setDeviceCmd = &cobra.Command{ Short: "Shows tui to pick active device", Long: `Allows setting or changing the active spotify device, shown in a tui`, Run: func(cmd *cobra.Command, args []string) { - tui.DisplayDevices(ctx, client) + tui.StartTea(ctx, client, "devices") }, } diff --git a/cmd/tracks.go b/cmd/tracks.go index e6cdc77..3456b0e 100644 --- a/cmd/tracks.go +++ b/cmd/tracks.go @@ -1,6 +1,9 @@ package cmd import ( + "os" + "path/filepath" + "gospt/internal/tui" "github.com/spf13/cobra" @@ -14,7 +17,11 @@ var tracksCmd = &cobra.Command{ Use: "tracks", Short: "Opens saved tracks", Long: `Uses TUI to open a list of saved tracks`, - Run: func(cmd *cobra.Command, args []string) { - tui.DisplayList(ctx, client) + RunE: func(cmd *cobra.Command, args []string) error { + configDir, _ := os.UserConfigDir() + if _, err := os.Stat(filepath.Join(configDir, "gospt/device.json")); err != nil { + return tui.StartTea(ctx, client, "devices") + } + return tui.StartTea(ctx, client, "tracks") }, } diff --git a/cmd/tui.go b/cmd/tui.go index 46580c1..a3edbbe 100644 --- a/cmd/tui.go +++ b/cmd/tui.go @@ -20,8 +20,8 @@ var tuiCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { configDir, _ := os.UserConfigDir() if _, err := os.Stat(filepath.Join(configDir, "gospt/device.json")); err != nil { - return tui.StartTea(ctx, client) + return tui.StartTea(ctx, client, "devices") } - return tui.DisplayMain(ctx, client) + return tui.StartTea(ctx, client, "main") }, } diff --git a/debug.log b/debug.log new file mode 100644 index 0000000..e69de29 diff --git a/internal/commands/commands.go b/internal/commands/commands.go index ec077f4..66e182d 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -489,7 +489,6 @@ func SetDevice(ctx *gctx.Context, client *spotify.Client, device spotify.PlayerD if err != nil { return err } - fmt.Println("Your device has been set to: ", device.Name) return nil } diff --git a/internal/tui/device.go b/internal/tui/device.go deleted file mode 100644 index 9414e58..0000000 --- a/internal/tui/device.go +++ /dev/null @@ -1,105 +0,0 @@ -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" { - 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()) - } - err = DisplayMain(m.ctx, m.client) - if err != nil { - return m, tea.Quit - } - return m, tea.Quit - } - case tea.MouseMsg: - if msg.Type == 5 { - m.list.CursorUp() - } - if msg.Type == 6 { - m.list.CursorDown() - } - - 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(), tea.WithMouseCellMotion()) - - if _, err := p.Run(); err != nil { - fmt.Println("Error running program:", err) - os.Exit(1) - } - fmt.Println("DEVICE SET AND SAVED") - return nil -} diff --git a/internal/tui/list.go b/internal/tui/list.goasdg similarity index 98% rename from internal/tui/list.go rename to internal/tui/list.goasdg index cab7e28..f481ed1 100644 --- a/internal/tui/list.go +++ b/internal/tui/list.goasdg @@ -82,7 +82,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: if msg.String() == "backspace" || msg.String() == "q" || msg.String() == "esc" { - m, err := InitMain(m.ctx, m.client) + m, err := InitMain(m.ctx, m.client, "main") if err != nil { fmt.Println("UH OH") } diff --git a/internal/tui/main.go b/internal/tui/main.go index 1a1a75d..683463b 100644 --- a/internal/tui/main.go +++ b/internal/tui/main.go @@ -3,21 +3,35 @@ package tui import ( "fmt" "os" + "time" "gospt/internal/commands" "gospt/internal/gctx" + "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" "github.com/zmb3/spotify/v2" ) -var main_updates chan *mainModel +var ( + currentlyPlaying string + main_updates chan *mainModel + page = 1 + + docStyle = lipgloss.NewStyle().Margin(1, 2) +) type mainItem struct { Name string + Duration string + Artist spotify.SimpleArtist + ID spotify.ID Desc string SpotifyItem any + Device spotify.PlayerDevice + spotify.SavedTrack } func (i mainItem) Title() string { return i.Name } @@ -25,43 +39,135 @@ func (i mainItem) Description() string { return i.Desc } func (i mainItem) FilterValue() string { return i.Title() + i.Desc } type mainModel struct { - list list.Model - page int - ctx *gctx.Context - client *spotify.Client + list list.Model + ctx *gctx.Context + client *spotify.Client + mode string + playlist spotify.SimplePlaylist } func (m mainModel) Init() tea.Cmd { + main_updates = make(chan *mainModel) return nil } -func (m *mainModel) LoadMoreItems() { - playlists, err := commands.Playlists(m.ctx, m.client, (m.page + 1)) +func (m *mainModel) Tick() { + ticker := time.NewTicker(5 * time.Second) + quit := make(chan struct{}) + go func() { + for { + select { + case <-ticker.C: + playing, _ := m.client.PlayerCurrentlyPlaying(m.ctx) + currentlyPlaying = "Now playing " + playing.Item.Name + " by " + playing.Item.Artists[0].Name + case <-quit: + ticker.Stop() + return + } + } + }() +} + +func HandlePlay(ctx *gctx.Context, client *spotify.Client, uri *spotify.URI, pos int) { + var err error + err = commands.PlaySongInPlaylist(ctx, client, uri, pos) if err != nil { + fmt.Println() + os.Exit(1) + } +} + +func HandleRadio(ctx *gctx.Context, client *spotify.Client, id spotify.ID) { + err := commands.RadioGivenSong(ctx, client, id, 0) + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func (m *mainModel) LoadMoreItems() { + switch m.mode { + case "main": + playlists, err := commands.Playlists(m.ctx, m.client, (page + 1)) + page++ + if err != nil { + return + } + items := []list.Item{} + for _, playlist := range playlists.Playlists { + items = append(items, mainItem{ + Name: playlist.Name, + Desc: playlist.Description, + SpotifyItem: playlist, + }) + } + for _, item := range items { + m.list.InsertItem(len(m.list.Items())+1, item) + } + main_updates <- m + return + case "playlist": + tracks, err := commands.PlaylistTracks(m.ctx, m.client, m.playlist.ID, (page + 1)) + page++ + if err != nil { + return + } + items := []mainItem{} + for _, track := range tracks.Tracks { + items = append(items, mainItem{ + Name: track.Track.Name, + Artist: track.Track.Artists[0], + Duration: track.Track.TimeDuration().Round(time.Second).String(), + ID: track.Track.ID, + Desc: track.Track.Artists[0].Name + " - " + track.Track.TimeDuration().Round(time.Second).String(), + }) + } + for _, item := range items { + m.list.InsertItem(len(m.list.Items())+1, item) + } + main_updates <- m + return + case "tracks": + tracks, err := commands.TrackList(m.ctx, m.client, (page + 1)) + page++ + if err != nil { + return + } + page++ + items := []list.Item{} + for _, track := range tracks.Tracks { + items = append(items, mainItem{ + Name: track.Name, + Artist: track.Artists[0], + Duration: track.TimeDuration().Round(time.Second).String(), + ID: track.ID, + Desc: track.Artists[0].Name + " - " + track.TimeDuration().Round(time.Second).String(), + }) + } + for _, item := range items { + m.list.InsertItem(len(m.list.Items())+1, item) + } + main_updates <- m return } - m.page++ - items := []list.Item{} - for _, playlist := range playlists.Playlists { - items = append(items, mainItem{ - Name: playlist.Name, - Desc: playlist.Description, - SpotifyItem: playlist, - }) +} + +func HandlePlayLikedSong(ctx *gctx.Context, client *spotify.Client, position int) { + err := commands.PlayLikedSongs(ctx, client, position) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) } - for _, item := range items { - m.list.InsertItem(len(m.list.Items())+1, item) - } - main_updates <- m } func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + m.list.NewStatusMessage(currentlyPlaying) select { - case msg := <-main_updates: - m.list.SetItems(msg.list.Items()) + case update := <-main_updates: + m.list.SetItems(update.list.Items()) default: } - if m.list.Paginator.Page == m.list.Paginator.TotalPages-2 { + if m.list.Paginator.Page == m.list.Paginator.TotalPages-2 && m.list.Cursor() == 0 { // if last request was still full request more if len(m.list.Items())%50 == 0 { go m.LoadMoreItems() @@ -69,34 +175,101 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } switch msg := msg.(type) { case tea.KeyMsg: - if msg.String() == "ctrl+c" || msg.String() == "q" || msg.String() == "esc" { - return m, tea.Quit + if msg.String() == "d" { + m.mode = "devices" + new_items, err := DeviceView(m.ctx, m.client) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + m.list.SetItems(new_items) + m.list.ResetSelected() + m.list.NewStatusMessage("Setting view to devices") } - if msg.String() == "enter" { - switch m.list.SelectedItem().(mainItem).SpotifyItem.(type) { - case spotify.SimplePlaylist: - playlist := m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimplePlaylist) - p, err := InitPlaylists(m.ctx, m.client, playlist) + if msg.String() == "backspace" { + if m.mode == "playlist" || m.mode == "tracks" || m.mode == "devices" { + m.mode = "main" + m.list.NewStatusMessage("Setting view to main") + new_items, err := MainView(m.ctx, m.client) if err != nil { - return m, tea.Quit - } - play := tea.NewProgram(p, tea.WithAltScreen(), tea.WithMouseCellMotion()) - if _, err := play.Run(); err != nil { - return m, tea.Quit - } - case *spotify.SavedTrackPage: - p, err := InitSavedTracks(m.ctx, m.client) - if err != nil { - return m, tea.Quit - } - play := tea.NewProgram(p, tea.WithAltScreen(), tea.WithMouseCellMotion()) - if _, err := play.Run(); err != nil { - return m, tea.Quit + fmt.Println(err.Error()) } + m.list.SetItems(new_items) + } else { return m, tea.Quit } + m.list.ResetSelected() + } + if msg.String() == "ctrl+c" { return m, tea.Quit } + if msg.String() == "enter" || msg.String() == "spacebar" { + switch m.mode { + case "main": + switch m.list.SelectedItem().(mainItem).SpotifyItem.(type) { + case spotify.SimplePlaylist: + m.mode = "playlist" + playlist := m.list.SelectedItem().(mainItem).SpotifyItem.(spotify.SimplePlaylist) + m.playlist = playlist + m.list.NewStatusMessage("Setting view to playlist " + playlist.Name) + new_items, err := PlaylistView(m.ctx, m.client, playlist) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + m.list.SetItems(new_items) + m.list.ResetSelected() + case *spotify.SavedTrackPage: + m.mode = "tracks" + m.list.NewStatusMessage("Setting view to saved tracks") + new_items, err := SavedTracksView(m.ctx, m.client) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + m.list.SetItems(new_items) + m.list.ResetSelected() + m.list.NewStatusMessage("Setting view to tracks") + } + case "playlist": + currentlyPlaying = m.list.SelectedItem().FilterValue() + m.list.NewStatusMessage("Playing " + currentlyPlaying) + go HandlePlay(m.ctx, m.client, &m.playlist.URI, m.list.Cursor()+(m.list.Paginator.Page*m.list.Paginator.PerPage)) + case "tracks": + currentlyPlaying = m.list.SelectedItem().FilterValue() + m.list.NewStatusMessage("Playing " + currentlyPlaying) + go HandlePlayLikedSong(m.ctx, m.client, m.list.Cursor()+(m.list.Paginator.Page*m.list.Paginator.PerPage)) + case "devices": + go HandleSetDevice(m.ctx, m.client, m.list.SelectedItem().(mainItem).Device) + m.list.NewStatusMessage("Setting device to " + m.list.SelectedItem().FilterValue()) + m.mode = "main" + m.list.NewStatusMessage("Setting view to main") + new_items, err := MainView(m.ctx, m.client) + if err != nil { + fmt.Println(err.Error()) + } + m.list.SetItems(new_items) + } + } + if msg.String() == "ctrl+r" { + switch m.mode { + case "main": + switch m.list.SelectedItem().(mainItem).SpotifyItem.(type) { + case spotify.SimplePlaylist: + m.list.NewStatusMessage("Not implemented yet") + case *spotify.SavedTrackPage: + m.list.NewStatusMessage("Not implemented yet") + } + case "playlist": + currentlyPlaying = m.list.SelectedItem().FilterValue() + m.list.NewStatusMessage("Starting radio for " + currentlyPlaying) + go HandleRadio(m.ctx, m.client, m.list.SelectedItem().(mainItem).ID) + case "tracks": + currentlyPlaying = m.list.SelectedItem().FilterValue() + m.list.NewStatusMessage("Playing " + currentlyPlaying) + go HandleRadio(m.ctx, m.client, m.list.SelectedItem().(mainItem).ID) + } + } case tea.MouseMsg: if msg.Type == 5 { m.list.CursorUp() @@ -139,11 +312,12 @@ func DisplayMain(ctx *gctx.Context, client *spotify.Client) error { } m := mainModel{ list: list.New(items, list.NewDefaultDelegate(), 0, 0), - page: 1, ctx: ctx, client: client, + mode: "main", } m.list.Title = "GOSPT" + go m.Tick() p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion()) @@ -154,7 +328,44 @@ func DisplayMain(ctx *gctx.Context, client *spotify.Client) error { return nil } -func InitMain(ctx *gctx.Context, client *spotify.Client) (tea.Model, error) { +func PlaylistView(ctx *gctx.Context, client *spotify.Client, playlist spotify.SimplePlaylist) ([]list.Item, error) { + items := []list.Item{} + tracks, err := commands.PlaylistTracks(ctx, client, playlist.ID, 1) + if err != nil { + return nil, err + } + for _, track := range tracks.Tracks { + items = append(items, mainItem{ + Name: track.Track.Name, + Artist: track.Track.Artists[0], + Duration: track.Track.TimeDuration().Round(time.Second).String(), + ID: track.Track.ID, + Desc: track.Track.Artists[0].Name + " - " + track.Track.TimeDuration().Round(time.Second).String(), + }) + } + return items, nil +} + +func SavedTracksView(ctx *gctx.Context, client *spotify.Client) ([]list.Item, error) { + items := []list.Item{} + tracks, err := commands.TrackList(ctx, client, 1) + if err != nil { + return nil, err + } + for _, track := range tracks.Tracks { + items = append(items, mainItem{ + Name: track.Name, + Artist: track.Artists[0], + Duration: track.TimeDuration().Round(time.Second).String(), + ID: track.ID, + Desc: track.Artists[0].Name + " - " + track.TimeDuration().Round(time.Second).String(), + }) + } + + return items, err +} + +func MainView(ctx *gctx.Context, client *spotify.Client) ([]list.Item, error) { items := []list.Item{} saved_items, err := commands.TrackList(ctx, client, 1) items = append(items, mainItem{ @@ -173,12 +384,68 @@ func InitMain(ctx *gctx.Context, client *spotify.Client) (tea.Model, error) { SpotifyItem: playlist, }) } + return items, nil +} + +func InitMain(ctx *gctx.Context, client *spotify.Client, mode string) (tea.Model, error) { + items := []list.Item{} + var err error + switch mode { + case "main": + items, err = MainView(ctx, client) + if err != nil { + return nil, err + } + case "devices": + items, err = DeviceView(ctx, client) + if err != nil { + return nil, err + } + case "tracks": + items, err = SavedTracksView(ctx, client) + if err != nil { + return nil, err + } + } m := mainModel{ list: list.New(items, list.NewDefaultDelegate(), 0, 0), - page: 1, ctx: ctx, client: client, + mode: mode, } m.list.Title = "GOSPT" + go m.Tick() + m.list.AdditionalShortHelpKeys = func() []key.Binding { + return []key.Binding{ + key.NewBinding(key.WithKeys("ctrl"+"r"), key.WithHelp("ctrl+r", "start radio")), + key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "select device")), + } + } return m, nil } + +func DeviceView(ctx *gctx.Context, client *spotify.Client) ([]list.Item, error) { + items := []list.Item{} + devices, err := client.PlayerDevices(ctx) + if err != nil { + return nil, err + } + for _, device := range devices { + items = append(items, mainItem{ + Name: device.Name, + Desc: fmt.Sprintf("%s - active: %t", device.ID, device.Active), + Device: device, + }) + } + return items, nil +} + +func HandleSetDevice(ctx *gctx.Context, client *spotify.Client, player spotify.PlayerDevice) { + fmt.Println("WHOA") + var err error + err = commands.SetDevice(ctx, client, player) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } +} diff --git a/internal/tui/playlists.go b/internal/tui/playlists.goasdg similarity index 100% rename from internal/tui/playlists.go rename to internal/tui/playlists.goasdg diff --git a/internal/tui/playlisttracks.go b/internal/tui/playlisttracks.jasdg similarity index 87% rename from internal/tui/playlisttracks.go rename to internal/tui/playlisttracks.jasdg index 8ddc5b7..c0afcd9 100644 --- a/internal/tui/playlisttracks.go +++ b/internal/tui/playlisttracks.jasdg @@ -43,6 +43,23 @@ func (m playlistTracksModel) Init() tea.Cmd { return nil } +func HandlePlay(ctx *gctx.Context, client *spotify.Client, uri *spotify.URI, pos int) { + var err error + err = commands.PlaySongInPlaylist(ctx, client, uri, pos) + if err != nil { + fmt.Println() + os.Exit(1) + } +} + +func HandleRadio(ctx *gctx.Context, client *spotify.Client, id spotify.ID) { + err := commands.RadioGivenSong(ctx, client, id, 0) + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} + func (m playlistTracksModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { select { case msg := <-list_updates: @@ -58,7 +75,7 @@ func (m playlistTracksModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: if msg.String() == "backspace" || msg.String() == "q" || msg.String() == "esc" { - m, err := InitMain(m.ctx, m.client) + m, err := InitMain(m.ctx, m.client, "main") if err != nil { fmt.Println("UH OH") } @@ -71,18 +88,11 @@ func (m playlistTracksModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit } if msg.String() == "ctrl+r" { - track := m.list.SelectedItem() - err := commands.RadioGivenSong(m.ctx, m.client, track.(item).ID, 0) - if err != nil { - return m, tea.Quit - } + go HandleRadio(m.ctx, m.client, m.list.SelectedItem().(item).ID) } if msg.String() == "enter" { - var err error - err = commands.PlaySongInPlaylist(m.ctx, m.client, &m.playlist.URI, m.list.Cursor()+(m.list.Paginator.Page*m.list.Paginator.PerPage)) - if err != nil { - m.ctx.Printf(err.Error()) - } + currentlyPlaying = "FUCK YOU" + go HandlePlay(m.ctx, m.client, &m.playlist.URI, m.list.Cursor()+(m.list.Paginator.Page*m.list.Paginator.PerPage)) } case tea.MouseMsg: if msg.Type == 5 { diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 678526f..0e82ab9 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -11,7 +11,7 @@ import ( ) // StartTea the entry point for the UI. Initializes the model. -func StartTea(ctx *gctx.Context, client *spotify.Client) error { +func StartTea(ctx *gctx.Context, client *spotify.Client, mode string) error { if f, err := tea.LogToFile("debug.log", "help"); err != nil { return err } else { @@ -22,7 +22,7 @@ func StartTea(ctx *gctx.Context, client *spotify.Client) error { } }() } - m, err := InitMain(ctx, client) + m, err := InitMain(ctx, client, mode) if err != nil { fmt.Println("UH OH") }