Audio player
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

250 lines
5.3 KiB

package main
import (
"flag"
"fmt"
"log"
"net/http"
_ "net/http/pprof"
"os"
"os/signal"
"path/filepath"
"runtime/pprof"
"strings"
"syscall"
"time"
"code.rocketnine.space/tslocum/ez"
)
const (
volumeBase = 2
versionInfo = `ditty - Audio player - v0.0.0
https://code.rocketnine.space/tslocum/ditty
The MIT License (MIT)
Copyright (c) 2020 Trevor Slocum <trevor@rocketnine.space>
`
)
var (
configPath string
printVersionInfo bool
bufferSize time.Duration
debugAddress string
cpuProfile string
streamFdInt int
streamFd *os.File
restrictLibrary string
showHiddenFolders bool
startingVolumeFlag int
disableAutoplay bool
disableMouse bool
version = "0.0.0"
done = make(chan bool)
)
func exit() {
done <- true
}
func main() {
log.SetFlags(0)
flag.StringVar(&configPath, "config", "", "path to configuration file")
flag.BoolVar(&printVersionInfo, "version", false, "print version information and exit")
flag.StringVar(&config.Layout, "layout", defaultLayout, "layout of interface elements")
flag.IntVar(&streamFdInt, "fd", -1, "stream audio to file descriptor")
flag.IntVar(&startingVolumeFlag, "volume", -1, "initial volume level 0-100")
flag.StringVar(&restrictLibrary, "restrict-library", "", "restrict library to path")
flag.BoolVar(&disableAutoplay, "disable-autoplay", false, "disable automatically playing the queue")
flag.BoolVar(&disableMouse, "disable-mouse", false, "disable mouse support")
flag.DurationVar(&bufferSize, "buffer-size", defaultBufferSize, "audio buffer size")
flag.StringVar(&debugAddress, "debug-address", "", "address to serve debug info")
flag.BoolVar(&showHiddenFolders, "hidden-folders", false, "show hidden folders")
flag.StringVar(&cpuProfile, "cpu-profile", "", "path to save CPU profiling")
flag.Parse()
if printVersionInfo {
fmt.Print(strings.Replace(versionInfo, "0.0.0", version, 1))
return
}
if restrictLibrary != "" {
var err error
restrictLibrary, err = filepath.Abs(restrictLibrary)
if err != nil {
panic(fmt.Sprintf("failed to restrict library to %s: %s", restrictLibrary, err))
}
}
if debugAddress != "" {
go func() {
panic(http.ListenAndServe(debugAddress, nil))
}()
}
if cpuProfile != "" {
f, err := os.Create(cpuProfile)
if err != nil {
panic(fmt.Sprintf("could not create CPU profile: %s", err))
}
defer f.Close()
if err := pprof.StartCPUProfile(f); err != nil {
panic(fmt.Sprintf("could not start CPU profile: %s", err))
}
defer pprof.StopCPUProfile()
}
if configPath == "" {
var err error
configPath, err = ez.DefaultConfigPath("ditty")
if err != nil {
panic(fmt.Sprintf("failed to determine default configuration file path: %s", err))
}
}
err := ez.Deserialize(config, configPath)
if err != nil {
panic(fmt.Sprintf("failed to read configuration file: %s", err))
}
if startingVolumeFlag >= 0 {
config.Volume = startingVolumeFlag
}
err = setKeyBinds()
if err != nil {
panic(fmt.Sprintf("failed to set keybinds: %s", err))
}
err = initTUI()
if err != nil {
panic(fmt.Sprintf("failed to initialize terminal user interface: %s", err))
}
defer app.HandlePanic()
sigc := make(chan os.Signal, 1)
signal.Notify(sigc,
syscall.SIGINT,
syscall.SIGTERM)
go func() {
<-sigc
done <- true
}()
go func() {
err := app.Run()
if err != nil {
panic(err)
}
done <- true
}()
var vol float64
if config.Volume == 0 {
vol = -7.5
} else {
vol = -7.5 + float64(7.5)*(float64(config.Volume)/float64(100))
if vol < -7.0 {
vol = -7.0
}
}
setVolume(roundUnit(vol, 0.5))
startPath := strings.Join(flag.Args(), " ")
if startPath == "" {
if restrictLibrary != "" {
startPath = restrictLibrary
} else if config.Remember && config.Dir != "" {
startPath = config.Dir
} else {
wd, err := os.Getwd()
if err != nil || wd == "" {
homeDir, err := os.UserHomeDir()
if err == nil && homeDir != "" {
startPath = homeDir
}
} else {
startPath = wd
}
}
}
if startPath == "" {
panic("supply a path to browse")
}
fileInfo, err := os.Stat(startPath)
if err != nil {
panic(err)
}
var playing bool
if fileInfo.IsDir() {
browseFolder(startPath)
} else {
browseFolder(filepath.Dir(startPath))
audioFile, err := openFile(strings.Join(flag.Args(), " "), nil)
if err != nil {
statusText = err.Error()
app.QueueUpdateDraw(updateMain)
} else {
play(audioFile)
playing = true
}
}
if config.Remember && config.QueueFiles != nil {
queueFiles = config.QueueFiles
app.QueueUpdateDraw(func() {
updateQueue()
queueList.SetCurrentItem(config.QueuePlaying)
if len(queueFiles) > 0 && !playing {
pauseNext = true
seekNext = config.AudioPosition
go queueSelect(config.QueuePlaying)
}
})
}
if config.Remember {
go func() {
t := time.NewTicker(5 * time.Minute)
for range t.C {
saveAppState()
ez.Serialize(config, configPath) // Failing isn't an issue, this could be logged though
}
}()
}
defer func() {
if app != nil {
app.Stop()
}
}()
t := time.NewTicker(time.Second)
for {
select {
case <-done:
if config.Remember {
saveAppState()
} else {
clearAppState()
}
ez.Serialize(config, configPath) // Failing isn't an issue, this could be logged though
if streamFd != nil {
streamFd.Close()
}
return
case <-t.C:
updateStatus()
}
}
}