Added basic music playback + tui
This commit is contained in:
13
pkg/commands.go
Normal file
13
pkg/commands.go
Normal 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
32
pkg/list_delegate.go
Normal 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
77
pkg/model.go
Normal 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
54
pkg/status_bar.go
Normal 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
72
pkg/track_fetcher.go
Normal 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
32
pkg/track_player.go
Normal 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
53
pkg/track_player_view.go
Normal 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
17
pkg/utils.go
Normal 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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user