Last active
March 26, 2022 23:12
-
-
Save orels1/a7de78c90019114c75f43e7a24d92fb1 to your computer and use it in GitHub Desktop.
Revisions
-
orels1 revised this gist
Mar 26, 2022 . 2 changed files with 234 additions and 235 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -2,5 +2,4 @@ For this program to work - you'll need to get a spotify auth token, which you ca _Made as a way to learn [bubbletea TUI library](https://github.com/charmbracelet/bubbletea)_  This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,18 +1,18 @@ package main import ( "encoding/json" "fmt" "io/ioutil" "log" "net/http" "os" "strings" "time" "github.com/charmbracelet/bubbles/progress" "github.com/charmbracelet/bubbles/spinner" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) @@ -28,83 +28,83 @@ var ( focusedButton = focusedStyle.Copy().Render("[ Load Tracks ]") blurredButton = fmt.Sprintf("[ %s ]", blurredStyle.Render("Load Tracks")) headerStyle = lipgloss.NewStyle(). Bold(true). Background(lipgloss.Color("#38363a")). Foreground(lipgloss.Color("#4af2a1")). PaddingTop(1). PaddingBottom(1). PaddingLeft(3). PaddingRight(3) boldStyle = lipgloss.NewStyle().Bold(true) ) type model struct { playlistUrl string authToken string fileName string focusIndex int inputs []textinput.Model cursorMode textinput.CursorMode spinner spinner.Model loadingTracks bool loadedTracks bool playlistInfo SpotifyTracksResp progress progress.Model exporting bool exportDone bool tracksData []SpotifyTrack } type SpotifyTracksResp struct { Offset int `json:"offset"` Limit int `json:"limit"` Total int `json:"total"` Items []SpotifyTrackItems `json:"items"` } type SpotifyTrackItems struct { Track SpotifyTrack `json:"track"` } type SpotifyTrack struct { Name string `json:"name"` Id string `json:"id"` Href string `json:"href"` Album SpotifyAlbum `json:"album"` Artists []SpotifyArtist `json:"artists"` } type SpotifyAlbum struct { Name string `json:"name"` Id string `json:"id"` Href string `json:"href"` } type SpotifyArtist struct { Name string `json:"name"` Id string `json:"id"` Href string `json:"href"` } type playlistData struct { data SpotifyTracksResp } type exportStep struct { data SpotifyTracksResp offset int } func initialModel() model { s := spinner.New() s.Spinner = spinner.MiniDot s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#4af2a1")) m := model{ inputs: make([]textinput.Model, 3), spinner: s, progress: progress.New(progress.WithDefaultGradient()), } var t textinput.Model @@ -131,16 +131,16 @@ func initialModel() model { } func (m model) Init() tea.Cmd { cmds := make([]tea.Cmd, 2) cmds[0] = textinput.Blink cmds[1] = m.spinner.Tick return tea.Batch(cmds...) } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.progress.Width = msg.Width - 2*2 - 4 if m.progress.Width > 80 { m.progress.Width = 80 } @@ -153,20 +153,20 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "tab", "shift+tab", "enter", "up", "down": s := msg.String() if !m.loadedTracks && s == "enter" && m.focusIndex == len(m.inputs) { m.loadingTracks = true m.focusIndex = 0 return m, loadTracks(m.inputs[0].Value(), m.inputs[1].Value()) } if m.loadedTracks && !m.exportDone && !m.exporting && s == "enter" { m.exporting = true return m, exportTracks(m.inputs[0].Value(), 0, m.inputs[1].Value()) } if m.exportDone && s == "enter" { return m, tea.Quit } // Cycle indexes if s == "up" || s == "shift+tab" { @@ -197,45 +197,45 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } return m, tea.Batch(cmds...) } case playlistData: m.loadingTracks = false m.loadedTracks = true m.playlistInfo = msg.data m.tracksData = make([]SpotifyTrack, m.playlistInfo.Total) return m, nil case exportStep: for i := 0; i < 100; i++ { if i < len(msg.data.Items) { m.tracksData[i+msg.offset] = msg.data.Items[i].Track } } if (msg.offset + 100) < m.playlistInfo.Total { cmd := m.progress.IncrPercent(100.0 / float64(m.playlistInfo.Total)) return m, tea.Batch(exportTracks(m.inputs[0].Value(), msg.offset+100, m.inputs[1].Value()), cmd) } m.exporting = false m.exportDone = true bytes, _ := json.MarshalIndent(m.tracksData, "", "\t") ioutil.WriteFile(m.inputs[2].Value(), bytes, 0644) return m, m.progress.IncrPercent(100.0 / float64(m.playlistInfo.Total)) case progress.FrameMsg: progressModel, cmd := m.progress.Update(msg) m.progress = progressModel.(progress.Model) return m, cmd } cmds := make([]tea.Cmd, 2) cmds[0] = m.updateInputs(msg) m.spinner, cmds[1] = m.spinner.Update(msg) return m, tea.Batch(cmds...) } func (m *model) updateInputs(msg tea.Msg) tea.Cmd { var cmds = make([]tea.Cmd, len(m.inputs)) for i := range m.inputs { m.inputs[i], cmds[i] = m.inputs[i].Update(msg) } @@ -244,140 +244,140 @@ func (m *model) updateInputs(msg tea.Msg) tea.Cmd { } func loadTracks(playlistId string, authToken string) tea.Cmd { return func() tea.Msg { client := &http.Client{} url := fmt.Sprintf("https://api.spotify.com/v1/playlists/%s/tracks?limit=10&offset=0&fields=offset,limit,total,items(track(name,id,href,release_date,album(name,id,href),artists(name,href,id)))", playlistId) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", authToken)) req.Header.Set("Content-Type", "application/json") response, err := client.Do(req) if err != nil { fmt.Print(err.Error()) os.Exit(1) } responseData, err := ioutil.ReadAll(response.Body) if err != nil { log.Fatal(err) } var parsed SpotifyTracksResp err = json.Unmarshal(responseData, &parsed) if err != nil { log.Fatal(err) } return playlistData{ data: parsed, } } } func exportTracks(playlistId string, offset int, authToken string) tea.Cmd { return func() tea.Msg { client := &http.Client{} url := fmt.Sprintf("https://api.spotify.com/v1/playlists/%s/tracks?limit=100&offset=%d&fields=offset,limit,total,items(track(name,id,href,release_date,album(name,id,href),artists(name,href,id)))", playlistId, offset) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", authToken)) req.Header.Set("Content-Type", "application/json") response, err := client.Do(req) if err != nil { fmt.Print(err.Error()) os.Exit(1) } responseData, err := ioutil.ReadAll(response.Body) if err != nil { log.Fatal(err) } var parsed SpotifyTracksResp err = json.Unmarshal(responseData, &parsed) if err != nil { log.Fatal(err) } time.Sleep(1 * time.Second) return exportStep{ data: parsed, offset: offset, } } } func (m model) View() string { var b strings.Builder b.WriteRune('\n') b.WriteString(headerStyle.Render("🎧 Spotify Track Exporter")) b.WriteRune('\n') b.WriteRune('\n') if !m.loadingTracks && !m.loadedTracks { b.WriteString(boldStyle.Render("First, we need some information")) b.WriteRune('\n') b.WriteRune('\n') } if !m.loadedTracks && !m.loadingTracks { for i := range m.inputs { b.WriteString(m.inputs[i].View()) if i < len(m.inputs)-1 { b.WriteRune('\n') } } button := &blurredButton if m.focusIndex == len(m.inputs) { button = &focusedButton } fmt.Fprintf(&b, "\n\n%s\n\n", *button) } if m.loadingTracks { b.WriteString(m.spinner.View()) b.WriteString(" Loading Playlist Data...") b.WriteRune('\n') } if m.loadedTracks && !m.exporting && !m.exportDone { b.WriteString(fmt.Sprintf("Found %d songs", m.playlistInfo.Total)) b.WriteRune('\n') b.WriteRune('\n') b.WriteString("Ready to export! Press Enter to start") b.WriteRune('\n') b.WriteRune('\n') b.WriteRune('\n') b.WriteString(focusedStyle.Copy().Render("[ Export ]")) b.WriteRune('\n') } if m.exporting && !m.exportDone { b.WriteRune('\n') b.WriteRune('\n') b.WriteString(m.progress.View()) b.WriteRune('\n') b.WriteRune('\n') } if m.exportDone { b.WriteString(fmt.Sprintf("Done! You can find your spotify tracks at %s", m.inputs[2].Value())) b.WriteRune('\n') b.WriteRune('\n') b.WriteString(focusedStyle.Copy().Render("[ Exit ]")) b.WriteRune('\n') } return b.String() } func main() { m := initialModel() p := tea.NewProgram(m) if err := p.Start(); err != nil { fmt.Printf("Alas, there has been an error: %v", err) os.Exit(1) } } -
orels1 revised this gist
Mar 26, 2022 . 1 changed file with 2 additions and 18 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -114,7 +114,7 @@ func initialModel() model { switch i { case 0: t.Placeholder = "Playlist ID" t.Focus() t.PromptStyle = focusedStyle t.TextStyle = focusedStyle @@ -135,7 +135,6 @@ func (m model) Init() tea.Cmd { cmds[0] = textinput.Blink cmds[1] = m.spinner.Tick return tea.Batch(cmds...) } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -151,19 +150,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "ctrl+c", "esc": return m, tea.Quit case "tab", "shift+tab", "enter", "up", "down": s := msg.String() @@ -249,9 +235,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *model) updateInputs(msg tea.Msg) tea.Cmd { var cmds = make([]tea.Cmd, len(m.inputs)) for i := range m.inputs { m.inputs[i], cmds[i] = m.inputs[i].Update(msg) } -
orels1 created this gist
Mar 26, 2022 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,6 @@ For this program to work - you'll need to get a spotify auth token, which you can get via many cli and UI tools, including things like `spotify-token`. This program does not cover getting the token for you _Made as a way to learn [bubbletea TUI library](https://github.com/charmbracelet/bubbletea)_  This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,399 @@ package main import ( "fmt" "os" "strings" "io/ioutil" "net/http" "log" "encoding/json" "time" "github.com/charmbracelet/bubbles/progress" "github.com/charmbracelet/bubbles/spinner" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) var ( focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) cursorStyle = focusedStyle.Copy() noStyle = lipgloss.NewStyle() helpStyle = blurredStyle.Copy() cursorModeHelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244")) focusedButton = focusedStyle.Copy().Render("[ Load Tracks ]") blurredButton = fmt.Sprintf("[ %s ]", blurredStyle.Render("Load Tracks")) headerStyle = lipgloss.NewStyle(). Bold(true). Background(lipgloss.Color("#38363a")). Foreground(lipgloss.Color("#4af2a1")). PaddingTop(1). PaddingBottom(1). PaddingLeft(3). PaddingRight(3) boldStyle = lipgloss.NewStyle().Bold(true) ) type model struct { playlistUrl string authToken string fileName string focusIndex int inputs []textinput.Model cursorMode textinput.CursorMode spinner spinner.Model loadingTracks bool loadedTracks bool playlistInfo SpotifyTracksResp progress progress.Model exporting bool exportDone bool tracksData []SpotifyTrack } type SpotifyTracksResp struct { Offset int `json:"offset"` Limit int `json:"limit"` Total int `json:"total"` Items []SpotifyTrackItems `json:"items"` } type SpotifyTrackItems struct { Track SpotifyTrack `json:"track"` } type SpotifyTrack struct { Name string `json:"name"` Id string `json:"id"` Href string `json:"href"` Album SpotifyAlbum `json:"album"` Artists []SpotifyArtist `json:"artists"` } type SpotifyAlbum struct { Name string `json:"name"` Id string `json:"id"` Href string `json:"href"` } type SpotifyArtist struct { Name string `json:"name"` Id string `json:"id"` Href string `json:"href"` } type playlistData struct { data SpotifyTracksResp } type exportStep struct { data SpotifyTracksResp offset int } func initialModel() model { s := spinner.New() s.Spinner = spinner.MiniDot s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("#4af2a1")) m := model{ inputs: make([]textinput.Model, 3), spinner: s, progress: progress.New(progress.WithDefaultGradient()), } var t textinput.Model for i := range m.inputs { t = textinput.New() t.CursorStyle = cursorStyle switch i { case 0: t.Placeholder = "Playlist URL" t.Focus() t.PromptStyle = focusedStyle t.TextStyle = focusedStyle case 1: t.Placeholder = "Auth Token" case 2: t.Placeholder = "File Name" } m.inputs[i] = t } return m } func (m model) Init() tea.Cmd { cmds := make([]tea.Cmd, 2) cmds[0] = textinput.Blink cmds[1] = m.spinner.Tick return tea.Batch(cmds...) // return m.spinner.Tick } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.progress.Width = msg.Width - 2*2 - 4 if m.progress.Width > 80 { m.progress.Width = 80 } return m, nil case tea.KeyMsg: switch msg.String() { case "ctrl+c", "esc": return m, tea.Quit // Change cursor mode case "ctrl+r": m.cursorMode++ if m.cursorMode > textinput.CursorHide { m.cursorMode = textinput.CursorBlink } cmds := make([]tea.Cmd, len(m.inputs)) for i := range m.inputs { cmds[i] = m.inputs[i].SetCursorMode(m.cursorMode) } return m, tea.Batch(cmds...) // Set focus to next input case "tab", "shift+tab", "enter", "up", "down": s := msg.String() if !m.loadedTracks && s == "enter" && m.focusIndex == len(m.inputs) { m.loadingTracks = true m.focusIndex = 0 return m, loadTracks(m.inputs[0].Value(), m.inputs[1].Value()) } if m.loadedTracks && !m.exportDone && !m.exporting && s == "enter" { m.exporting = true; return m, exportTracks(m.inputs[0].Value(), 0, m.inputs[1].Value()) } if m.exportDone && s == "enter" { return m, tea.Quit } // Cycle indexes if s == "up" || s == "shift+tab" { m.focusIndex-- } else { m.focusIndex++ } if m.focusIndex > len(m.inputs) { m.focusIndex = 0 } else if m.focusIndex < 0 { m.focusIndex = len(m.inputs) } cmds := make([]tea.Cmd, len(m.inputs)) for i := 0; i <= len(m.inputs)-1; i++ { if i == m.focusIndex { // Set focused state cmds[i] = m.inputs[i].Focus() m.inputs[i].PromptStyle = focusedStyle m.inputs[i].TextStyle = focusedStyle continue } // Remove focused state m.inputs[i].Blur() m.inputs[i].PromptStyle = noStyle m.inputs[i].TextStyle = noStyle } return m, tea.Batch(cmds...) } case playlistData: m.loadingTracks = false m.loadedTracks = true m.playlistInfo = msg.data m.tracksData = make([]SpotifyTrack, m.playlistInfo.Total) return m, nil case exportStep: for i := 0; i < 100; i++ { if (i < len(msg.data.Items)) { m.tracksData[i + msg.offset] = msg.data.Items[i].Track } } if (msg.offset + 100) < m.playlistInfo.Total { cmd := m.progress.IncrPercent(100.0 / float64(m.playlistInfo.Total)) return m, tea.Batch(exportTracks(m.inputs[0].Value(), msg.offset + 100, m.inputs[1].Value()), cmd) } m.exporting = false m.exportDone = true bytes, _ := json.MarshalIndent(m.tracksData, "", "\t") ioutil.WriteFile(m.inputs[2].Value(), bytes, 0644) return m, m.progress.IncrPercent(100.0 / float64(m.playlistInfo.Total)) case progress.FrameMsg: progressModel, cmd := m.progress.Update(msg) m.progress = progressModel.(progress.Model) return m, cmd } cmds := make([]tea.Cmd, 2) cmds[0] = m.updateInputs(msg) m.spinner, cmds[1] = m.spinner.Update(msg) return m, tea.Batch(cmds...) } func (m *model) updateInputs(msg tea.Msg) tea.Cmd { var cmds = make([]tea.Cmd, len(m.inputs)) // Only text inputs with Focus() set will respond, so it's safe to simply // update all of them here without any further logic. for i := range m.inputs { m.inputs[i], cmds[i] = m.inputs[i].Update(msg) } return tea.Batch(cmds...) } func loadTracks(playlistId string, authToken string) tea.Cmd { return func() tea.Msg { client := &http.Client{} url := fmt.Sprintf("https://api.spotify.com/v1/playlists/%s/tracks?limit=10&offset=0&fields=offset,limit,total,items(track(name,id,href,release_date,album(name,id,href),artists(name,href,id)))", playlistId) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", authToken)) req.Header.Set("Content-Type", "application/json") response, err := client.Do(req) if err != nil { fmt.Print(err.Error()) os.Exit(1) } responseData, err := ioutil.ReadAll(response.Body) if err != nil { log.Fatal(err) } var parsed SpotifyTracksResp err = json.Unmarshal(responseData, &parsed) if err != nil { log.Fatal(err) } return playlistData { data: parsed, } } } func exportTracks(playlistId string, offset int, authToken string) tea.Cmd { return func() tea.Msg { client := &http.Client{} url := fmt.Sprintf("https://api.spotify.com/v1/playlists/%s/tracks?limit=100&offset=%d&fields=offset,limit,total,items(track(name,id,href,release_date,album(name,id,href),artists(name,href,id)))", playlistId, offset) req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", authToken)) req.Header.Set("Content-Type", "application/json") response, err := client.Do(req) if err != nil { fmt.Print(err.Error()) os.Exit(1) } responseData, err := ioutil.ReadAll(response.Body) if err != nil { log.Fatal(err) } var parsed SpotifyTracksResp err = json.Unmarshal(responseData, &parsed) if err != nil { log.Fatal(err) } time.Sleep(1 * time.Second) return exportStep { data: parsed, offset: offset, } } } func (m model) View() string { var b strings.Builder b.WriteRune('\n') b.WriteString(headerStyle.Render("🎧 Spotify Track Exporter")) b.WriteRune('\n') b.WriteRune('\n') if !m.loadingTracks && !m.loadedTracks { b.WriteString(boldStyle.Render("First, we need some information")) b.WriteRune('\n') b.WriteRune('\n') } if !m.loadedTracks && !m.loadingTracks { for i := range m.inputs { b.WriteString(m.inputs[i].View()) if i < len(m.inputs)-1 { b.WriteRune('\n') } } button := &blurredButton if m.focusIndex == len(m.inputs) { button = &focusedButton } fmt.Fprintf(&b, "\n\n%s\n\n", *button) } if m.loadingTracks { b.WriteString(m.spinner.View()) b.WriteString(" Loading Playlist Data...") b.WriteRune('\n') } if m.loadedTracks && !m.exporting && !m.exportDone { b.WriteString(fmt.Sprintf("Found %d songs", m.playlistInfo.Total)) b.WriteRune('\n') b.WriteRune('\n') b.WriteString("Ready to export! Press Enter to start") b.WriteRune('\n') b.WriteRune('\n') b.WriteRune('\n') b.WriteString(focusedStyle.Copy().Render("[ Export ]")) b.WriteRune('\n') } if m.exporting && !m.exportDone { b.WriteRune('\n') b.WriteRune('\n') b.WriteString(m.progress.View()) b.WriteRune('\n') b.WriteRune('\n') } if m.exportDone { b.WriteString(fmt.Sprintf("Done! You can find your spotify tracks at %s", m.inputs[2].Value())) b.WriteRune('\n') b.WriteRune('\n') b.WriteString(focusedStyle.Copy().Render("[ Exit ]")) b.WriteRune('\n') } return b.String() } func main() { m := initialModel() p := tea.NewProgram(m) if err := p.Start(); err != nil { fmt.Printf("Alas, there has been an error: %v", err) os.Exit(1) } }