342
package main
import (
"fmt"
"time"
"errors"
"github.com/faiface/beep"
"github.com/faiface/beep/mp3"
"github.com/faiface/beep/speaker"
"golang.org/x/term"
"math/rand"
"os"
"path/filepath"
"strings"
)
var ErrInvalidFile = errors.New("invalid or inaccessible file")
var ErrUnsupportedFileType = errors.New("unsupported file type")
type MusicFile struct {
Name string
Path string
Type int8
}
type Playlist []MusicFile
func GetMF(path string) (*MusicFile, error) {
ext := filepath.Ext(path)
if ext != ".mp3" {
return nil, ErrUnsupportedFileType
}
return &MusicFile{
Name: strings.TrimSuffix(filepath.Base(path), ext),
Path: path,
}, nil
}
func (pl *Playlist) addMp3(path string) error {
mf, err := GetMF(path)
if err != nil {
return err
}
var st os.FileInfo
st, err = os.Stat(mf.Path)
if err != nil || st.IsDir() || !st.Mode().IsRegular() {
return ErrInvalidFile
}
*pl = append(*pl, *mf)
return nil
}
func (pl *Playlist) addDir(dir string) error {
entries, err := os.ReadDir(dir)
if err != nil {
return err
}
for _, entry := range entries {
if entry.IsDir() {
continue
}
pl.addMp3(filepath.Join(dir, entry.Name()))
}
return nil
}
func (pl *Playlist) AddFiles(paths ...string) {
var fi os.FileInfo
var err error
for _, path := range paths {
fi, err = os.Stat(path)
if err != nil {
continue
}
if fi.IsDir() {
_ = pl.addDir(path)
} else if fi.Mode().IsRegular() {
_ = pl.addMp3(path)
}
}
}
func (pl *Playlist) Len() int {
return len(*pl)
}
func (pl *Playlist) IsEmpty() bool {
return len(*pl) == 0
}
type Queue struct {
Shuffle bool
Repeat bool
UpNext []MusicFile
Source *Playlist
}
func (q *Queue) Fill() {
l := q.Source.Len()
q.UpNext = make([]MusicFile, l)
if q.Shuffle {
for i, v := range rand.Perm(l) {
q.UpNext[v] = (*q.Source)[i]
}
} else {
copy(q.UpNext, *q.Source)
}
}
func (pl *Playlist) Player(shuffle, repeat bool) *Player {
player = &Player{
Q: &Queue{
Shuffle: shuffle,
Repeat: repeat,
Source: pl,
},
nx: make(chan struct{}),
end: make(chan struct{}),
}
return player
}
func (q *Queue) Next() *MusicFile {
if len(q.UpNext) == 0 {
if q.Repeat {
q.Fill()
} else {
return nil
}
}
next := q.UpNext[0]
q.UpNext = q.UpNext[1:]
return &next
}
type Player struct {
Q *Queue
Np *MusicFile
skip func()
ctrl beep.Ctrl
nx, end chan struct{}
}
func (p *Player) TogglePause() {
if p.ctrl.Paused {
p.ctrl.Paused = false
} else {
p.ctrl.Paused = true
}
}
func (p *Player) Skip() {
if p.skip != nil {
p.skip()
}
}
func (p *Player) play(song *MusicFile) (err error) {
speaker.Clear()
wait := make(chan struct{})
var streamer beep.StreamSeekCloser
var format beep.Format
file, err := os.Open(song.Path)
if err != nil {
goto ret
}
streamer, format, err = mp3.Decode(file)
if err != nil {
goto retOne
}
err = speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10))
if err != nil {
goto retTwo
}
p.skip = func() {
p.skip = nil
wait <- struct{}{}
close(wait)
}
p.ctrl = beep.Ctrl{Streamer: streamer, Paused: p.ctrl.Paused}
speaker.Play(beep.Seq(&p.ctrl, beep.Callback(p.skip)))
<-wait
speaker.Clear()
retTwo:
streamer.Close()
retOne:
file.Close()
ret:
return err
}
func (p *Player) Play(nx chan struct{}) (err error) {
p.Q.Fill()
p.end = make(chan struct{})
for {
p.Np = p.Q.Next()
if p.Np == nil {
break
}
nx <- struct{}{}
err = p.play(p.Np)
if err != nil {
return err
}
}
close(nx)
p.end <- struct{}{}
close(p.end)
return nil
}
func (p *Player) Quit() {
p.Q.Repeat = false
p.Q.UpNext = []MusicFile{}
p.Np = nil
p.Skip()
<-p.end
}
var player *Player
func safeExit(code int) {
player.Quit()
DisableRawMode()
time.Sleep(20 * time.Millisecond)
os.Exit(code)
}
func main() {
var err error
if len(os.Args) < 2 {
fmt.Printf("Usage: %s <path>...\n", filepath.Base(os.Args[0]))
fmt.Printf("Files must be mp3, directories will be scanned for mp3 files.\n")
os.Exit(1)
}
var playlist Playlist
playlist.AddFiles(os.Args[1:]...)
if len(playlist) == 0 {
fmt.Println("No supported music files found in the directory.")
os.Exit(0)
}
playlist.Player(true, true)
EnableRawMode()
defer DisableRawMode()
go func() {
var er error
var n int
buf := make([]byte, 4)
for {
if n, er = os.Stdin.Read(buf); er != nil {
safeExit(1)
}
switch string(buf[:n]) {
case "q", "Q", "\x03", "\x04":
player.Quit()
case " ":
player.TogglePause()
case "s", "S":
player.Skip()
}
}
}()
nx := make(chan struct{})
go func() {
for ok := true; ok; _, ok = <-nx {
fmt.Print("\033[2J\033[1;1H") // Clear screen
fmt.Print("Controls: [space] Pause/Play | [s] Skip | [q] Quit\n\r")
if player.Np == nil {
fmt.Print("No song is currently playing.\n\r")
} else {
fmt.Printf("Now Playing: %s\n\r", player.Np.Name)
}
fmt.Print("Up Next:\n\r")
if len(player.Q.UpNext) == 0 {
fmt.Print(" (queue is empty)\n\r")
continue
}
for i, song := range player.Q.UpNext {
if i >= 5 {
fmt.Printf(" ...and %d more\n\r", len(player.Q.UpNext)-i)
break
}
fmt.Printf(" %d. %s\n\r", i+1, song.Name)
}
}
}()
err = player.Play(nx)
if err != nil {
fmt.Println("An error occured during playback:", err.Error())
safeExit(1)
}
safeExit(0)
}
var oldState *term.State
var rawEnabled bool
func EnableRawMode() {
if rawEnabled {
return
}
rawEnabled = true
var err error
oldState, err = term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
fmt.Println("An error occured while setting terminal to raw mode.")
os.Exit(1)
}
fmt.Print("\033[?1049h")
}
func DisableRawMode() {
if !rawEnabled {
return
}
rawEnabled = false
if r := recover(); r != nil {
term.Restore(int(os.Stdin.Fd()), oldState) // restore terminal when program exits
fmt.Print("\033[?1049l")
panic(r)
} else {
term.Restore(int(os.Stdin.Fd()), oldState) // restore terminal when program exits
fmt.Print("\033[?1049l")
}
}