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 ` ) 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() } } }