From de4c66871025653617ee2230fe5ecf3d6d2a91ea Mon Sep 17 00:00:00 2001 From: jjohnstondev Date: Sun, 8 Jan 2023 19:44:35 -0800 Subject: [PATCH] playlist browser --- internal/api/api.go | 2 + internal/commands/commands.go | 133 +++++++++++++++--------------- internal/tui/device.go | 10 ++- internal/tui/list.go | 59 +++++++++----- internal/tui/playlists.go | 123 ++++++++++++++++++++++++++++ internal/tui/playlisttracks.go | 145 +++++++++++++++++++++++++++++++++ 6 files changed, 383 insertions(+), 89 deletions(-) create mode 100644 internal/tui/playlists.go create mode 100644 internal/tui/playlisttracks.go diff --git a/internal/api/api.go b/internal/api/api.go index 7a4314c..3983987 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -48,6 +48,8 @@ func Run(ctx *gctx.Context, client *spotify.Client, args []string) error { return commands.RefillRadio(ctx, client) case "tracks": return tui.DisplayList(ctx, client) + case "playlists": + return tui.DisplayPlaylists(ctx, client) case "status": return commands.Status(ctx, client) case "devices": diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 74083b4..b3fd758 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -32,7 +32,6 @@ func Play(ctx *gctx.Context, client *spotify.Client) error { return err } } - ctx.Println("Playing!") return nil } @@ -60,7 +59,6 @@ func PlayUrl(ctx *gctx.Context, client *spotify.Client, args []string) error { if err != nil { return err } - ctx.Println("Playing!") return nil } else { return err @@ -70,7 +68,6 @@ func PlayUrl(ctx *gctx.Context, client *spotify.Client, args []string) error { if err != nil { return err } - ctx.Println("Playing!") return nil } @@ -86,13 +83,66 @@ func QueueSong(ctx *gctx.Context, client *spotify.Client, id spotify.ID) error { if err != nil { return err } - ctx.Println("Queued!") return nil } else { return err } } - ctx.Println("Queued!") + return nil +} + +func RadioGivenSong(ctx *gctx.Context, client *spotify.Client, song_id spotify.ID) error { + seed := spotify.Seeds{ + Tracks: []spotify.ID{song_id}, + } + recomendations, err := client.GetRecommendations(ctx, 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 = ClearRadio(ctx, client) + if err != nil { + return err + } + radioPlaylist, err := GetRadioPlaylist(ctx, client) + if err != nil { + return err + } + _, err = client.AddTracksToPlaylist(ctx, radioPlaylist.ID, recomendationIds...) + if err != nil { + return err + } + client.PlayOpt(ctx, &spotify.PlayOptions{ + PlaybackContext: &radioPlaylist.URI, + PlaybackOffset: &spotify.PlaybackOffset{ + Position: 0, + }, + }) + err = client.Repeat(ctx, "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]}, + } + additional_recs, err := client.GetRecommendations(ctx, seed, &spotify.TrackAttributes{}, spotify.Limit(100)) + if err != nil { + return err + } + additionalRecsIds := []spotify.ID{} + for _, song := range additional_recs.Tracks { + additionalRecsIds = append(additionalRecsIds, song.ID) + } + _, err = client.AddTracksToPlaylist(ctx, radioPlaylist.ID, additionalRecsIds...) + if err != nil { + return err + } + } return nil } @@ -126,62 +176,7 @@ func Radio(ctx *gctx.Context, client *spotify.Client) error { seed_song = tracks.Tracks[rand.Intn(len(tracks.Tracks))].SimpleTrack } } - - seed := spotify.Seeds{ - Tracks: []spotify.ID{seed_song.ID}, - } - fmt.Println("GETTING RECOMENDATIONS FOR", seed_song.Name) - recomendations, err := client.GetRecommendations(ctx, 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 = ClearRadio(ctx, client) - if err != nil { - return err - } - radioPlaylist, err := GetRadioPlaylist(ctx, client) - if err != nil { - return err - } - _, err = client.AddTracksToPlaylist(ctx, radioPlaylist.ID, recomendationIds...) - if err != nil { - return err - } - client.PlayOpt(ctx, &spotify.PlayOptions{ - PlaybackContext: &radioPlaylist.URI, - PlaybackOffset: &spotify.PlaybackOffset{ - Position: 0, - }, - }) - err = client.Repeat(ctx, "context") - if err != nil { - return err - } - fmt.Println("RADIO STARTED") - for i := 0; i < 4; i++ { - id := rand.Intn(len(recomendationIds)-2) + 1 - seed := spotify.Seeds{ - Tracks: []spotify.ID{recomendationIds[id]}, - } - additional_recs, err := client.GetRecommendations(ctx, seed, &spotify.TrackAttributes{}, spotify.Limit(100)) - if err != nil { - return err - } - additionalRecsIds := []spotify.ID{} - for _, song := range additional_recs.Tracks { - additionalRecsIds = append(additionalRecsIds, song.ID) - } - _, err = client.AddTracksToPlaylist(ctx, radioPlaylist.ID, additionalRecsIds...) - if err != nil { - return err - } - } - fmt.Println("500 TRACKS QUEUED") - return nil + return RadioGivenSong(ctx, client, seed_song.ID) } func RefillRadio(ctx *gctx.Context, client *spotify.Client) error { @@ -209,7 +204,6 @@ func RefillRadio(ctx *gctx.Context, client *spotify.Client) error { } recomendationIds := []spotify.ID{} if len(to_remove) > 0 { - fmt.Println("REPLENISHING", len(to_remove), "SONGS") _, err = client.RemoveTracksFromPlaylist(ctx, radioPlaylist.ID, to_remove...) if err != nil { return err @@ -236,7 +230,6 @@ func RefillRadio(ctx *gctx.Context, client *spotify.Client) error { return err } } - fmt.Println("RADIO REPLENISHED") return nil } @@ -252,7 +245,6 @@ func ClearRadio(ctx *gctx.Context, client *spotify.Client) error { configDir, _ := os.UserConfigDir() os.Remove(filepath.Join(configDir, "gospt/radio.json")) client.Pause(ctx) - fmt.Println("Radio emptied") return nil } @@ -269,7 +261,6 @@ func Pause(ctx *gctx.Context, client *spotify.Client) error { if err != nil { return err } - ctx.Println("Pausing!") return nil } @@ -293,7 +284,6 @@ func Like(ctx *gctx.Context, client *spotify.Client) error { if err != nil { return err } - ctx.Println("Pausing!") return nil } @@ -306,7 +296,6 @@ func Unlike(ctx *gctx.Context, client *spotify.Client) error { if err != nil { return err } - ctx.Println("Pausing!") return nil } @@ -315,7 +304,6 @@ func Skip(ctx *gctx.Context, client *spotify.Client) error { if err != nil { return err } - ctx.Println("Skipping!") return nil } @@ -324,7 +312,6 @@ func Previous(ctx *gctx.Context, client *spotify.Client) error { if err != nil { return err } - ctx.Println("Previous!") return nil } @@ -372,6 +359,14 @@ func TrackList(ctx *gctx.Context, client *spotify.Client, page int) (*spotify.Sa return client.CurrentUsersTracks(ctx, spotify.Limit(50), spotify.Offset((page-1)*50)) } +func Playlists(ctx *gctx.Context, client *spotify.Client, page int) (*spotify.SimplePlaylistPage, error) { + return client.CurrentUsersPlaylists(ctx, spotify.Limit(50), spotify.Offset((page-1)*50)) +} + +func PlaylistTracks(ctx *gctx.Context, client *spotify.Client, playlist spotify.ID, page int) (*spotify.PlaylistTrackPage, error) { + return client.GetPlaylistTracks(ctx, playlist, spotify.Limit(50), spotify.Offset((page-1)*50)) +} + func PrintState(state *spotify.PlayerState) error { state.Item.AvailableMarkets = []string{} state.Item.Album.AvailableMarkets = []string{} diff --git a/internal/tui/device.go b/internal/tui/device.go index 3309703..b883890 100644 --- a/internal/tui/device.go +++ b/internal/tui/device.go @@ -53,6 +53,14 @@ func (m deviceModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { fmt.Println("DEVICE SET") 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) @@ -84,7 +92,7 @@ func DisplayDevices(ctx *gctx.Context, client *spotify.Client) error { 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()) + p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion()) if _, err := p.Run(); err != nil { fmt.Println("Error running program:", err) diff --git a/internal/tui/list.go b/internal/tui/list.go index 1d6307a..a9ff493 100644 --- a/internal/tui/list.go +++ b/internal/tui/list.go @@ -43,22 +43,25 @@ func (m model) Init() tea.Cmd { func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.list.Paginator.OnLastPage() { - tracks, err := commands.TrackList(m.ctx, m.client, (m.page + 1)) - if err != nil { - return m, tea.Quit - } - m.page++ - items := []list.Item{} - for _, track := range tracks.Tracks { - items = append(items, item{ - Name: track.Name, - Artist: track.Artists[0], - Duration: track.TimeDuration().Round(time.Second).String(), - ID: track.ID, - }) - } - for _, item := range items { - m.list.InsertItem(len(m.list.Items())+1, item) + // if last request was still full request more + if len(m.list.Items())%50 == 0 { + tracks, err := commands.TrackList(m.ctx, m.client, (m.page + 1)) + if err != nil { + return m, tea.Quit + } + m.page++ + items := []list.Item{} + for _, track := range tracks.Tracks { + items = append(items, item{ + Name: track.Name, + Artist: track.Artists[0], + Duration: track.TimeDuration().Round(time.Second).String(), + ID: track.ID, + }) + } + for _, item := range items { + m.list.InsertItem(len(m.list.Items())+1, item) + } } } switch msg := msg.(type) { @@ -66,6 +69,13 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.String() == "ctrl+c" { return m, tea.Quit } + if msg.String() == "ctrl+r" { + track := m.list.SelectedItem() + err := commands.RadioGivenSong(m.ctx, m.client, track.(item).ID) + if err != nil { + return m, tea.Quit + } + } if msg.String() == "enter" { track := m.list.SelectedItem() var err error @@ -77,7 +87,13 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if err != nil { m.ctx.Printf(err.Error()) } - + } + case tea.MouseMsg: + if msg.Type == 5 { + m.list.CursorUp() + } + if msg.Type == 6 { + m.list.CursorDown() } case tea.WindowSizeMsg: h, v := docStyle.GetFrameSize() @@ -108,10 +124,15 @@ func DisplayList(ctx *gctx.Context, client *spotify.Client) error { }) } - m := model{list: list.New(items, list.NewDefaultDelegate(), 0, 0), page: 1, ctx: ctx, client: client} + m := model{ + 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()) + p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion()) if _, err := p.Run(); err != nil { fmt.Println("Error running program:", err) diff --git a/internal/tui/playlists.go b/internal/tui/playlists.go new file mode 100644 index 0000000..ca9c95b --- /dev/null +++ b/internal/tui/playlists.go @@ -0,0 +1,123 @@ +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 playlistsDocStyle = lipgloss.NewStyle().Margin(1, 2) + +type playlistItem struct { + Name string + Desc string + ID spotify.ID + spotify.SimplePlaylist +} + +func (i playlistItem) Title() string { return i.Name } +func (i playlistItem) Description() string { return i.Desc } +func (i playlistItem) FilterValue() string { return i.Title() + i.Desc } + +type playlistModel struct { + list list.Model + page int + ctx *gctx.Context + client *spotify.Client +} + +func (m playlistModel) Init() tea.Cmd { + return nil +} + +func (m playlistModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if m.list.Paginator.OnLastPage() { + // if the last request was not full + if len(m.list.Items())%50 == 0 { + playlists, err := commands.Playlists(m.ctx, m.client, (m.page + 1)) + if err != nil { + return m, tea.Quit + } + m.page++ + items := []list.Item{} + for _, playlist := range playlists.Playlists { + items = append(items, playlistItem{ + Name: playlist.Name, + Desc: playlist.Description, + ID: playlist.ID, + SimplePlaylist: playlist, + }) + } + for _, item := range items { + m.list.InsertItem(len(m.list.Items())+1, item) + } + } + } + switch msg := msg.(type) { + case tea.KeyMsg: + if msg.String() == "ctrl+c" { + return m, tea.Quit + } + if msg.String() == "enter" { + playlist := m.list.SelectedItem().(playlistItem).SimplePlaylist + PlaylistTracks(m.ctx, m.client, playlist) + 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 playlistModel) View() string { + return docStyle.Render(m.list.View()) +} + +func DisplayPlaylists(ctx *gctx.Context, client *spotify.Client) error { + items := []list.Item{} + playlists, err := commands.Playlists(ctx, client, 1) + if err != nil { + return err + } + for _, playlist := range playlists.Playlists { + items = append(items, playlistItem{ + Name: playlist.Name, + Desc: playlist.Description, + ID: playlist.ID, + SimplePlaylist: playlist, + }) + } + m := playlistModel{ + 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) + } + return nil +} diff --git a/internal/tui/playlisttracks.go b/internal/tui/playlisttracks.go new file mode 100644 index 0000000..01af03f --- /dev/null +++ b/internal/tui/playlisttracks.go @@ -0,0 +1,145 @@ +package tui + +import ( + "fmt" + "os" + "time" + + "gospt/internal/commands" + "gospt/internal/gctx" + + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/zmb3/spotify/v2" +) + +type track struct { + Name string + Duration string + Artist spotify.SimpleArtist + ID spotify.ID + spotify.SavedTrack +} + +func (i track) Title() string { return i.Name } +func (i track) Description() string { + return fmt.Sprint(i.Duration, " by ", i.Artist.Name) +} +func (i track) FilterValue() string { return i.Title() + i.Artist.Name } + +type playlistTracksModel struct { + list list.Model + page int + ctx *gctx.Context + client *spotify.Client + playlist spotify.SimplePlaylist +} + +func (m playlistTracksModel) Init() tea.Cmd { + return nil +} + +func (m playlistTracksModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if m.list.Paginator.OnLastPage() { + // if last request was still full request more + if len(m.list.Items())%50 == 0 { + tracks, err := commands.PlaylistTracks(m.ctx, m.client, m.playlist.ID, (m.page + 1)) + if err != nil { + return m, tea.Quit + } + m.page++ + items := []list.Item{} + for _, track := range tracks.Tracks { + items = append(items, item{ + Name: track.Track.Name, + Artist: track.Track.Artists[0], + Duration: track.Track.TimeDuration().Round(time.Second).String(), + ID: track.Track.ID, + }) + } + for _, item := range items { + m.list.InsertItem(len(m.list.Items())+1, item) + } + + } + } + switch msg := msg.(type) { + case tea.KeyMsg: + if msg.String() == "backspace" || msg.String() == "q" { + DisplayPlaylists(m.ctx, m.client) + } + if msg.String() == "ctrl+c" { + return m, tea.Quit + } + if msg.String() == "ctrl+r" { + track := m.list.SelectedItem() + err := commands.RadioGivenSong(m.ctx, m.client, track.(item).ID) + if err != nil { + return m, tea.Quit + } + } + if msg.String() == "enter" { + track := m.list.SelectedItem() + var err error + err = commands.QueueSong(m.ctx, m.client, track.(item).ID) + if err != nil { + m.ctx.Printf(err.Error()) + } + err = commands.Skip(m.ctx, m.client) + if err != nil { + m.ctx.Printf(err.Error()) + } + } + 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 playlistTracksModel) View() string { + return docStyle.Render(m.list.View()) +} + +func PlaylistTracks(ctx *gctx.Context, client *spotify.Client, playlist spotify.SimplePlaylist) error { + items := []list.Item{} + tracks, err := commands.PlaylistTracks(ctx, client, playlist.ID, 1) + if err != nil { + return err + } + for _, track := range tracks.Tracks { + items = append(items, item{ + Name: track.Track.Name, + Artist: track.Track.Artists[0], + Duration: track.Track.TimeDuration().Round(time.Second).String(), + ID: track.Track.ID, + }) + } + + m := playlistTracksModel{ + list: list.New(items, list.NewDefaultDelegate(), 0, 0), + page: 1, + ctx: ctx, + client: client, + playlist: playlist, + } + 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) + } + return nil +}