git.sophuwu.com > melgody   
              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")
	}
}