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 ...\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") } }