379
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: true,
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
Progress func() (string, float64)
skip func()
ctrl beep.Ctrl
end chan struct{}
nx bool
}
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) {
wait := make(chan struct{})
var streamer beep.StreamSeekCloser
var format beep.Format
var s string
var D float64
file, err := os.Open(song.Path)
if err != nil {
goto ret
}
streamer, format, err = mp3.Decode(file)
if err != nil {
goto retOne
}
D = float64(streamer.Len() * format.Precision / int(format.SampleRate) / format.NumChannels)
s = fmt.Sprintf("%2.2d:%2.2d", int(D)/60, int(D)%60)
p.Progress = func() (string, float64) {
d := float64(D) * float64(streamer.Position()) / float64(streamer.Len())
dd := int(d)
return fmt.Sprintf("%2.2d:%2.2d/%s", dd/60, dd%60, s), float64(d) / D
}
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() (err error) {
p.Q.Fill()
p.end = make(chan struct{})
for {
p.Np = p.Q.Next()
if p.Np == nil {
break
}
err = p.play(p.Np)
if err != nil {
return err
}
}
p.nx = false
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", "\x03", "\x04":
player.Quit()
case " ":
player.TogglePause()
case "s", "S":
player.Q.Shuffle = !player.Q.Shuffle
player.Q.Fill()
case "r", "R":
player.Q.Repeat = !player.Q.Repeat
case "N":
player.Skip()
}
}
}()
go func() {
var i int
var s, tstr string
var prog float64
var song MusicFile
var W, H int
for player.nx {
time.Sleep(200 * time.Millisecond)
W, H, _ = term.GetSize(int(os.Stdin.Fd()))
fmt.Printf("\033[2J\033[%d;1H", H-6) // Clear screen
if player.Np != nil {
fmt.Printf(" Now Playing: %s\n\r", player.Np.Name)
tstr, prog = player.Progress()
i = int(prog * float64(W-21))
fmt.Printf(" %s [%sO%s] %s\n\r", map[bool]string{true: "||", false: "▶ "}[player.ctrl.Paused], strings.Repeat("=", i), strings.Repeat("-", W-22-i), tstr)
} else {
fmt.Printf(" Now Playing: (none)\n\r")
fmt.Printf(" || [%s] 00:00/00:00\n\r", strings.Repeat("-", W-21))
}
fmt.Printf("\n")
fmt.Printf(" [SPACE] Pause/Play | Status: %8.8s\n\r", map[bool]string{true: "paused", false: "playing"}[player.ctrl.Paused])
fmt.Printf(" [SHIFT]+[Q] Quit | [SHIFT]+[N] Next\n\r")
mp := map[bool]string{true: "On ", false: "Off"}
fmt.Printf(" [S] Shuffle: %s | [R] Repeat: %s\n\r", mp[player.Q.Shuffle], mp[player.Q.Repeat])
s = ""
if len(player.Q.UpNext) == 0 {
s = " (empty)"
}
fmt.Printf("\033[%d;1H Queue %s\n\r", H-9, s)
for i, song = range player.Q.UpNext {
if H-10-i <= 2 {
fmt.Printf("\033[%d;1H %d more\n\r", H-10-i, len(player.Q.UpNext)-i)
break
}
fmt.Printf("\033[%d;1H%5d - %s\n\r", H-10-i, i+1, func(s string) string {
if len(s) > W-10 {
return s[:W-13] + "..."
}
return s
}(song.Name))
}
}
}()
err = player.Play()
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")
}
}