Added basic music playback + tui

This commit is contained in:
2022-03-20 19:50:55 +01:00
parent f9124deba7
commit 8fff93464a
13 changed files with 527 additions and 1 deletions

13
pkg/commands.go Normal file
View File

@ -0,0 +1,13 @@
package gomus
import tea "github.com/charmbracelet/bubbletea"
type trackChangeMsg struct {
nextTrack track
}
func newTrackChangeCmd(nextTrack track) tea.Cmd {
return func() tea.Msg {
return trackChangeMsg{nextTrack}
}
}

32
pkg/list_delegate.go Normal file
View File

@ -0,0 +1,32 @@
package gomus
import (
"fmt"
"io"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type trackListDelegate struct{}
func (d trackListDelegate) Height() int { return 1 }
func (d trackListDelegate) Spacing() int { return 0 }
func (d trackListDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
func (d trackListDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
t := listItem.(track)
f := t.fullName()
if m.Index() == index {
li := lipgloss.NewStyle().Bold(true).Render(f)
fmt.Fprintf(w, fmt.Sprintf("[>] %s", li))
return
}
fmt.Fprintf(w, f)
}
func newTrackListDelegate() trackListDelegate {
return trackListDelegate{}
}

77
pkg/model.go Normal file
View File

@ -0,0 +1,77 @@
package gomus
import (
"fmt"
"os"
tea "github.com/charmbracelet/bubbletea"
)
var (
termWidth = 0
termHeight = 0
)
type Model struct {
cursor int
currentlyPlaying int
trackPlayer
trackIndex
trackPlayerView trackPlayerView
}
func NewModel() Model {
if len(os.Args) < 2 {
fmt.Println("Expected a path to some music")
os.Exit(0)
}
ti := NewDirTrackIndex(os.Args[1])
tpv := newTrackPlayerView(ti.tracks)
return Model{
cursor: 0,
currentlyPlaying: 0,
trackIndex: ti,
trackPlayer: trackPlayer{},
trackPlayerView: tpv,
}
}
func (m Model) Init() tea.Cmd {
return tea.Batch(tea.EnterAltScreen, m.trackPlayerView.Init())
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
termHeight = msg.Height
termWidth = msg.Width
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
m.trackPlayer.close()
return m, tea.Quit
case "enter":
t := m.trackPlayerView.trackList.SelectedItem().(track)
cmds = append(cmds, newTrackChangeCmd(t))
m.trackPlayer.play(t.getReader())
}
}
tpv, cmd := m.trackPlayerView.Update(msg)
m.trackPlayerView = tpv
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
func (m Model) View() string {
return m.trackPlayerView.View()
}

54
pkg/status_bar.go Normal file
View File

@ -0,0 +1,54 @@
package gomus
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var (
statusBarStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#343433", Dark: "#C1C6B2"}).
Background(lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#353533"})
statusStyle = lipgloss.NewStyle().
Inherit(statusBarStyle).
Foreground(lipgloss.Color("#FFFDF5")).
Background(lipgloss.Color("#FF5F87")).
Padding(0, 1).
MarginRight(1)
statusText = lipgloss.NewStyle().Inherit(statusBarStyle)
)
type statusBar struct {
currentTrack track
}
func (s statusBar) Init() tea.Cmd {
return nil
}
func (s statusBar) View() string {
w := lipgloss.Width
statusKey := statusStyle.Render("NOW PLAYING")
statusVal := statusText.Copy().
Width(termWidth - w(statusKey)).
Render(s.currentTrack.fullName())
bar := lipgloss.JoinHorizontal(lipgloss.Top,
statusKey,
statusVal,
)
return statusBarStyle.Width(termWidth).Render(bar)
}
func (s statusBar) Update(msg tea.Msg) (statusBar, tea.Cmd) {
switch msg := msg.(type) {
case trackChangeMsg:
s.currentTrack = msg.nextTrack
}
return s, nil
}

72
pkg/track_fetcher.go Normal file
View File

@ -0,0 +1,72 @@
package gomus
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/mewkiz/flac"
"github.com/mewkiz/flac/meta"
)
type track struct {
name string
artist string
trackPath string
}
func (t track) FilterValue() string { return "" }
func (t track) fullName() string {
return fmt.Sprintf("%s - %s", t.artist, t.name)
}
func (t track) getReader() io.Reader {
f, err := os.Open(t.trackPath)
check(err)
return f
}
func trackFromFlac(path string) track {
s, err := flac.ParseFile(path)
check(err)
t := track{trackPath: path}
for _, block := range s.Blocks {
if block.Header.Type == meta.TypeVorbisComment {
c := block.Body.(*meta.VorbisComment)
for _, tag := range c.Tags {
if tag[0] == "ARTIST" {
t.artist = tag[1]
} else if tag[0] == "TITLE" {
t.name = tag[1]
}
}
}
}
return t
}
type trackIndex struct {
tracks []track
}
func NewDirTrackIndex(path string) trackIndex {
files, err := ioutil.ReadDir(path)
check(err)
tracks := []track{}
for _, file := range files {
if !file.IsDir() && strings.HasSuffix(file.Name(), ".flac") {
p := filepath.Join(path, file.Name())
tracks = append(tracks, trackFromFlac(p))
}
}
return trackIndex{tracks: tracks}
}

32
pkg/track_player.go Normal file
View File

@ -0,0 +1,32 @@
package gomus
import (
"io"
"time"
"github.com/faiface/beep"
"github.com/faiface/beep/flac"
"github.com/faiface/beep/speaker"
)
type trackPlayer struct {
currentStream beep.StreamSeekCloser
}
func (t trackPlayer) play(reader io.Reader) {
if t.currentStream != nil {
t.currentStream.Close()
}
streamer, format, err := flac.Decode(reader)
check(err)
err = speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))
check(err)
speaker.Play(streamer)
t.currentStream = streamer
}
func (t trackPlayer) close() {
t.currentStream.Close()
}

53
pkg/track_player_view.go Normal file
View File

@ -0,0 +1,53 @@
package gomus
import (
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type trackPlayerView struct {
trackList list.Model
statusBar
}
func newTrackPlayerView(tracks []track) trackPlayerView {
c := mapList(tracks, func(t track) list.Item {
return t
})
l := list.New(c, newTrackListDelegate(), 0, 0)
l.SetShowTitle(false)
l.SetShowStatusBar(false)
l.SetShowHelp(false)
l.SetFilteringEnabled(false)
return trackPlayerView{trackList: l}
}
func (v trackPlayerView) Init() tea.Cmd {
return nil
}
func (v trackPlayerView) View() string {
return lipgloss.JoinVertical(lipgloss.Left, v.trackList.View(), v.statusBar.View())
}
func (v trackPlayerView) Update(msg tea.Msg) (trackPlayerView, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
v.trackList.SetHeight(msg.Height - 2)
v.trackList.SetWidth(msg.Width)
}
tl, cmd := v.trackList.Update(msg)
v.trackList = tl
cmds = append(cmds, cmd)
sb, cmd := v.statusBar.Update(msg)
v.statusBar = sb
cmds = append(cmds, cmd)
return v, tea.Batch(cmds...)
}

17
pkg/utils.go Normal file
View File

@ -0,0 +1,17 @@
package gomus
import "log"
func mapList[T any, R any](l []T, f func(T) R) []R {
var c []R = []R{}
for _, item := range l {
c = append(c, f(item))
}
return c
}
func check(err error) {
if err != nil {
log.Fatal(err)
}
}