Add queue

Resolves #7.
This commit is contained in:
Trevor Slocum 2020-01-28 09:26:44 -08:00
parent c50589ef5f
commit 803053317a
10 changed files with 407 additions and 92 deletions

View file

@ -10,12 +10,6 @@ fmt:
- gofmt -l -s -e .
- exit $(gofmt -l -s -e . | wc -l)
lint:
stage: validate
script:
- go get -u golang.org/x/lint/golint
- golint -set_exit_status
vet:
stage: validate
script:

View file

@ -1,5 +1,6 @@
0.1.5:
- Add initial volume configuration option and flag
- Add queue feature
- Use default terminal colors
0.1.4:

View file

@ -2,8 +2,9 @@ This document covers the [ditty](https://gitlab.com/tslocum/ditty)
configuration options and their defaults.
# Options
* **--buffer-size** Audio buffer size, defaults to 500ms.
* **--autoplay-queue** Start playing when the queue is added to.
* **--restrict-library** Restrict access to a folder and its subfolders.
* **--fd** Write audio in WAV format to the specified file descriptor instead. This allows ditty to be used over ssh:
* `ssh ditty.rocketnine.space -t 'ditty --fd=2' 2> >(aplay --quiet)`
@ -13,13 +14,16 @@ configuration options and their defaults.
* **Select** Enter
* **Pause** Space
* **Refresh** R
* **Toggle hidden folder visibility** .
* **Browse parent folder and focus last** Backspace
* **Queue** Q
* **Delete from queue** D
* **Toggle focused list** Tab
* **Browse items** J/K, Down/Up and PageDown/PageUp
* **Previous track** P
* **Next track** N
* **Volume** -/+/M
* **Refresh** R
* **Toggle hidden folder visibility** .
* **Browse parent folder and focus last** Backspace
* **Exit** Escape
# Default ~/.config/ditty/config.yaml
@ -34,6 +38,12 @@ input:
- 'Space'
refresh:
- 'r'
queue:
- 'q'
delete:
- 'd'
focus-next:
- 'Tab'
hidden-folders:
- '.'
browse-parent:

View file

@ -30,9 +30,7 @@ Choose one of the following methods:
### Compile
```
go get gitlab.com/tslocum/ditty
```
```go get gitlab.com/tslocum/ditty```
## Dependencies
@ -49,4 +47,4 @@ See [CONFIGURATION.md](https://gitlab.com/tslocum/ditty/blob/master/CONFIGURATIO
## Support
Please share issues/suggestions [here](https://gitlab.com/tslocum/ditty/issues).
Please share issues and suggestions [here](https://gitlab.com/tslocum/ditty/issues).

View file

@ -165,8 +165,7 @@ func play(audioFile *audioFile) {
}
go app.QueueUpdateDraw(func() {
updateMain()
updateQueue()
updateLists()
updateStatus()
})
}
@ -187,10 +186,10 @@ func pause() {
}
func nextTrack() {
if mainBufferCursor-1 < len(mainBufferFiles)-1 {
mainBufferCursor++
if queueCursor < len(queueFiles)-1 {
queueCursor++
entry := selectedEntry()
entry := selectedQueueEntry()
audioFile, err := openFile(path.Join(mainBufferDirectory, entry.File.Name()), entry.Metadata)
if err != nil {
return
@ -202,25 +201,21 @@ func nextTrack() {
}
func skipPrevious() {
if mainBufferCursor > 1 {
if offsetEntry(-1).File.IsDir() {
return
}
listPrevious()
go listSelect()
if offsetQueueEntry(-1) == nil {
return
}
queuePrevious()
go queueSelect()
}
func skipNext() {
if mainBufferCursor < len(mainBufferFiles) {
if offsetEntry(1).File.IsDir() {
return
}
listNext()
go listSelect()
if offsetQueueEntry(1) == nil {
return
}
queueNext()
go queueSelect()
}
func roundUnit(x, unit float64) float64 {

127
gui.go
View file

@ -26,19 +26,30 @@ var (
bottomstatusbuf *cview.TextView
mainBufferFiles []*libraryEntry
mainBufferCursor int
mainBufferCursor = 1 // Position cursor on first entry
mainBufferDirectory string
mainBufferOrigin int
mainBufHeight int
mainBufferAutoFocus string // Entry path to focus after loading display
mainBuffer bytes.Buffer
mainLock = new(sync.Mutex)
queueFiles []*libraryEntry
queueCursor int
queueDirectory string
queueOrigin int
queueHeight int
queueBuffer bytes.Buffer
queueLock = new(sync.Mutex)
queueFocused bool
seekStart, seekEnd int
volumeStart, volumeEnd int
screenWidth, screenHeight int
mainBufHeight int
mainBuffer bytes.Buffer
mainLock = new(sync.Mutex)
statusText string
statusBuffer bytes.Buffer
@ -82,6 +93,11 @@ func initTUI() error {
return mainbuf.GetInnerRect()
})
queuebuf.SetDrawFunc(func(screen tcell.Screen, x, y, width, height int) (i int, i2 int, i3 int, i4 int) {
queueHeight = height
return queuebuf.GetInnerRect()
})
app.SetRoot(grid, true)
return nil
@ -204,21 +220,25 @@ func updateMain() {
var printed int
var line string
if mainBufferOrigin == 0 {
if mainBufferCursor == 0 {
mainBuffer.WriteString("[::r]")
}
writeListItemPrefix(&mainBuffer, !queueFocused, mainBufferCursor, 0)
if mainBufferDirectory == "/" {
line = "./"
} else {
line = "../"
}
mainBuffer.WriteString(line)
if queueFocused {
writeListItemSuffix(&mainBuffer, !queueFocused, mainBufferCursor, 0)
}
for i := len(line); i < screenWidth-2; i++ {
mainBuffer.WriteRune(' ')
}
if mainBufferCursor == 0 {
mainBuffer.WriteString("[-:-:-]")
if !queueFocused {
writeListItemSuffix(&mainBuffer, !queueFocused, mainBufferCursor, 0)
}
printed++
}
for i, entry := range mainBufferFiles {
@ -230,20 +250,25 @@ func updateMain() {
mainBuffer.WriteRune('\n')
}
if i == mainBufferCursor-1 {
mainBuffer.WriteString("[::r]")
}
writeListItemPrefix(&mainBuffer, !queueFocused, mainBufferCursor-1, i)
if entry.File.IsDir() {
line = entry.File.Name() + "/"
} else {
line = entry.String()
}
mainBuffer.WriteString(line)
if queueFocused {
writeListItemSuffix(&mainBuffer, !queueFocused, mainBufferCursor-1, i)
}
for i := runewidth.StringWidth(line); i < screenWidth-2; i++ {
mainBuffer.WriteRune(' ')
}
if i == mainBufferCursor-1 {
mainBuffer.WriteString("[-:-:-]")
if !queueFocused {
writeListItemSuffix(&mainBuffer, !queueFocused, mainBufferCursor-1, i)
}
printed++
@ -256,7 +281,77 @@ func updateMain() {
}
func updateQueue() {
// TODO
queueLock.Lock()
defer queueLock.Unlock()
queueBuffer.Reset()
var printed int
var line string
for i, entry := range queueFiles {
if i < queueOrigin || i-queueOrigin > queueHeight-1 {
continue
}
if printed > 0 {
queueBuffer.WriteRune('\n')
}
writeListItemPrefix(&queueBuffer, queueFocused, queueCursor, i)
if entry.File.IsDir() {
line = entry.File.Name() + "/"
} else {
line = entry.String()
}
queueBuffer.WriteString(line)
if !queueFocused {
writeListItemSuffix(&queueBuffer, queueFocused, queueCursor, i)
}
for i := runewidth.StringWidth(line); i < screenWidth-2; i++ {
queueBuffer.WriteRune(' ')
}
if queueFocused {
writeListItemSuffix(&queueBuffer, queueFocused, queueCursor, i)
}
printed++
if printed == queueHeight-2 {
break
}
}
queuebuf.SetText(queueBuffer.String())
}
func writeListItemPrefix(buffer *bytes.Buffer, focused bool, cursor int, i int) {
if focused {
if i == cursor {
buffer.WriteString("[::r]")
}
} else {
if i == cursor {
buffer.WriteString("[::bu]")
}
}
}
func writeListItemSuffix(buffer *bytes.Buffer, focused bool, cursor int, i int) {
if focused {
if i == cursor {
buffer.WriteString("[-:-:-]")
}
} else {
if i == cursor {
buffer.WriteString("[-:-:-]")
}
}
}
func updateLists() {
updateMain()
updateQueue()
}
func updateStatus() {

View file

@ -12,6 +12,9 @@ const (
actionSelect = "select"
actionPause = "pause"
actionRefresh = "refresh"
actionQueue = "queue"
actionDelete = "delete"
actionFocusNext = "focus-next"
actionToggleHidden = "hidden-folders"
actionBrowseParent = "browse-parent"
actionVolumeMute = "volume-mute"
@ -30,6 +33,9 @@ var actionHandlers = map[string]func(){
actionSelect: listSelect,
actionPause: pause,
actionRefresh: listRefresh,
actionQueue: listQueue,
actionDelete: listDelete,
actionFocusNext: toggleFocusedList,
actionToggleHidden: listToggleHidden,
actionBrowseParent: browseParent,
actionVolumeMute: toggleMute,
@ -87,6 +93,9 @@ func setDefaultKeyBinds() {
actionSelect: {"Enter"},
actionPause: {"Space"},
actionRefresh: {"r"},
actionQueue: {"q"},
actionDelete: {"d"},
actionFocusNext: {"Tab"},
actionToggleHidden: {"."},
actionBrowseParent: {"Backspace"},
actionVolumeMute: {"m"},

View file

@ -6,34 +6,83 @@ import (
)
func listPrevious() {
if mainBufferOrigin > 0 && mainBufferCursor == mainBufferOrigin {
mainBufferOrigin--
}
if mainBufferCursor > 0 {
mainBufferCursor--
}
go app.QueueUpdateDraw(updateMain)
}
func listNext() {
if mainBufferCursor < len(mainBufferFiles) {
mainBufferCursor++
if mainBufferCursor-mainBufferOrigin > mainBufHeight-3 {
mainBufferOrigin++
if !queueFocused {
if mainBufferOrigin > 0 && mainBufferCursor == mainBufferOrigin {
mainBufferOrigin--
}
if mainBufferCursor > 0 {
mainBufferCursor--
}
} else {
if queueOrigin > 0 && queueCursor == queueOrigin {
queueOrigin--
}
if queueCursor > 0 {
queueCursor--
}
}
go app.QueueUpdateDraw(updateMain)
go app.QueueUpdateDraw(updateLists)
}
func listNext() {
if !queueFocused {
if mainBufferCursor < len(mainBufferFiles) {
mainBufferCursor++
if mainBufferCursor-mainBufferOrigin > mainBufHeight-3 {
mainBufferOrigin++
}
}
} else {
if queueCursor < len(queueFiles)-1 {
queueCursor++
if queueCursor-queueOrigin > queueHeight-3 {
queueOrigin++
}
}
}
go app.QueueUpdateDraw(updateLists)
}
func queuePrevious() {
if queueOrigin > 0 && queueCursor == queueOrigin {
queueOrigin--
}
if queueCursor > 0 {
queueCursor--
}
go app.QueueUpdateDraw(updateQueue)
}
func queueNext() {
if queueCursor < len(queueFiles)-1 {
queueCursor++
queueOrigin = queueCursor - ((queueHeight - 3) / 2)
if queueOrigin < 0 {
queueOrigin = 0
}
if queueOrigin > (len(queueFiles)-1)-(queueHeight-3) {
queueOrigin = (len(queueFiles) - 1) - (queueHeight - 3)
}
}
go app.QueueUpdateDraw(updateQueue)
}
func listSelect() {
if queueFocused {
queueSelect()
return
}
if mainBufferCursor == 0 {
browseFolder(path.Join(mainBufferDirectory, ".."))
return
}
entry := selectedEntry()
entry := selectedMainEntry()
if entry.File.IsDir() {
browseFolder(path.Join(mainBufferDirectory, path.Base(entry.File.Name())))
return
@ -55,48 +104,179 @@ func listSelect() {
go app.QueueUpdateDraw(updateStatus)
}
func selectedEntry() *libraryEntry {
func queueSelect() {
entry := selectedQueueEntry()
if entry.File.IsDir() {
// TODO error
return
}
audioFile, err := openFile(entry.Path, entry.Metadata)
if err != nil {
statusText = err.Error()
go func() {
time.Sleep(5 * time.Second)
statusText = ""
go app.QueueUpdateDraw(updateMain)
}()
go app.QueueUpdateDraw(updateMain)
return
}
go play(audioFile)
go app.QueueUpdateDraw(updateStatus)
}
func listQueue() {
if mainBufferCursor == 0 {
// TODO Show error
return
}
entry := selectedMainEntry()
if entry == nil {
return
} else if entry.File.IsDir() {
scanFiles := scanFolderRecursively(entry.Path)
queueLock.Lock()
queueFiles = append(queueFiles, scanFiles...)
queueLock.Unlock()
if playOnQueue && playingFileID == 0 && len(queueFiles) > 0 {
queueSelect()
}
go app.QueueUpdateDraw(updateQueue)
return
}
queueLock.Lock()
defer queueLock.Unlock()
queueFiles = append(queueFiles, entry)
if playOnQueue && playingFileID == 0 {
queueSelect()
}
go app.QueueUpdateDraw(updateLists)
}
func listDelete() {
if !queueFocused {
return
}
queueLock.Lock()
defer queueLock.Unlock()
if len(queueFiles) <= queueCursor {
return
}
queueFiles = append(queueFiles[:queueCursor], queueFiles[queueCursor+1:]...)
if queueCursor > len(queueFiles)-1 {
queueCursor = len(queueFiles) - 1
if queueCursor < 0 {
queueCursor = 0
}
}
go app.QueueUpdateDraw(updateQueue)
}
func selectedMainEntry() *libraryEntry {
return mainBufferFiles[mainBufferCursor-1]
}
func offsetEntry(offset int) *libraryEntry {
func offsetMainEntry(offset int) *libraryEntry {
if (mainBufferCursor-1)+offset < 0 || (mainBufferCursor-1)+offset >= len(mainBufferFiles) {
return nil
}
return mainBufferFiles[(mainBufferCursor-1)+offset]
}
func selectedQueueEntry() *libraryEntry {
return queueFiles[queueCursor]
}
func offsetQueueEntry(offset int) *libraryEntry {
if queueCursor+offset < 0 || queueCursor+offset >= len(queueFiles) {
return nil
}
return queueFiles[queueCursor+offset]
}
func listPreviousPage() {
if mainBufferOrigin == 0 {
mainBufferCursor = 0
if !queueFocused {
if mainBufferOrigin == 0 {
mainBufferCursor = 0
go app.QueueUpdateDraw(updateMain)
return
}
mainBufferOrigin -= (mainBufHeight - 1) - 2
if mainBufferOrigin < 0 {
mainBufferOrigin = 0
}
mainBufferCursor = mainBufferOrigin
go app.QueueUpdateDraw(updateMain)
return
}
} else {
if queueOrigin == 0 {
queueCursor = 0
mainBufferOrigin -= (mainBufHeight - 1) - 2
if mainBufferOrigin < 0 {
mainBufferOrigin = 0
}
mainBufferCursor = mainBufferOrigin
go app.QueueUpdateDraw(updateQueue)
return
}
go app.QueueUpdateDraw(updateMain)
queueOrigin -= (queueHeight - 1) - 2
if queueOrigin < 0 {
queueOrigin = 0
}
queueCursor = queueOrigin
go app.QueueUpdateDraw(updateQueue)
}
}
func listNextPage() {
numEntries := len(mainBufferFiles)
if !queueFocused {
numEntries := len(mainBufferFiles)
if mainBufferOrigin >= numEntries-(mainBufHeight-1) {
mainBufferCursor = numEntries
if mainBufferOrigin >= numEntries-(mainBufHeight-1) {
mainBufferCursor = numEntries
go app.QueueUpdateDraw(updateMain)
return
}
mainBufferOrigin += (mainBufHeight - 1) - 2
if mainBufferOrigin > (numEntries-(mainBufHeight-1))+2 {
mainBufferOrigin = (numEntries - (mainBufHeight - 1)) + 2
}
mainBufferCursor = mainBufferOrigin
go app.QueueUpdateDraw(updateMain)
return
}
} else {
numEntries := len(queueFiles)
mainBufferOrigin += (mainBufHeight - 1) - 2
if mainBufferOrigin > (numEntries-(mainBufHeight-1))+2 {
mainBufferOrigin = (numEntries - (mainBufHeight - 1)) + 2
}
mainBufferCursor = mainBufferOrigin
if queueOrigin >= numEntries-(queueHeight-1) {
queueCursor = numEntries - 1
go app.QueueUpdateDraw(updateMain)
go app.QueueUpdateDraw(updateQueue)
return
}
queueOrigin += queueHeight - 2
if queueOrigin > ((numEntries-1)-(queueHeight-1))+2 {
queueOrigin = ((numEntries - 1) - (queueHeight - 1)) + 2
}
queueCursor = queueOrigin
go app.QueueUpdateDraw(updateQueue)
}
}
func listRefresh() {
@ -122,3 +302,9 @@ func listToggleHidden() {
listRefresh()
}
func toggleFocusedList() {
queueFocused = !queueFocused
go app.QueueUpdateDraw(updateLists)
}

View file

@ -36,6 +36,7 @@ func readMetadata(f *os.File) *metadata {
type libraryEntry struct {
File os.FileInfo
Path string
Metadata *metadata
}
@ -59,10 +60,12 @@ func scanFolder(scanPath string) []*libraryEntry {
var entries []*libraryEntry
for _, fileInfo := range files {
p := path.Join(scanPath, fileInfo.Name())
b := path.Base(fileInfo.Name())
if fileInfo.IsDir() {
if b != "" && (b[0] != '.' || showHiddenFolders) {
entries = append(entries, &libraryEntry{File: fileInfo, Metadata: &metadata{Title: fileInfo.Name()}})
entries = append(entries, &libraryEntry{File: fileInfo, Path: p, Metadata: &metadata{Title: fileInfo.Name()}})
}
continue
@ -70,14 +73,14 @@ func scanFolder(scanPath string) []*libraryEntry {
continue
}
f, err := os.Open(path.Join(scanPath, fileInfo.Name()))
f, err := os.Open(p)
if err != nil {
continue
}
metadata := readMetadata(f)
f.Close()
entries = append(entries, &libraryEntry{File: fileInfo, Metadata: metadata})
entries = append(entries, &libraryEntry{File: fileInfo, Path: p, Metadata: metadata})
}
sort.Slice(entries, func(i, j int) bool {
@ -94,3 +97,25 @@ func scanFolder(scanPath string) []*libraryEntry {
return entries
}
func scanFolderRecursively(path string) []*libraryEntry {
var entries []*libraryEntry
scanFiles := scanFolder(path)
for _, entry := range scanFiles {
if !entry.File.IsDir() {
continue
}
entries = append(entries, scanFolder(entry.Path)...)
}
for _, entry := range scanFiles {
if entry.File.IsDir() {
continue
}
entries = append(entries, entry)
}
return entries
}

View file

@ -36,6 +36,7 @@ var (
restrictLibrary string
showHiddenFolders bool
startingVolumeFlag int
playOnQueue bool
version = "0.0.0"
done = make(chan bool)
@ -53,6 +54,7 @@ func main() {
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(&playOnQueue, "autoplay-queue", true, "starting playing on initial queue")
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")