Merge branch 'master' into youtube-search

This commit is contained in:
raziman 2021-02-06 11:14:26 +08:00
commit f2f225a058
12 changed files with 475 additions and 169 deletions

View File

@ -15,6 +15,7 @@ Gomu is a Terminal User Interface **TUI** music player to play mp3 files from yo
- [youtube-dl](https://github.com/ytdl-org/youtube-dl) integration
- audio file management
- customizable
- find music from youtube
## Dependencies
If you are using ubuntu, you need to install alsa and required dependencies
@ -64,10 +65,19 @@ general:
confirm_on_exit: true
load_prev_queue: true
music_dir: ~/music
history_path: ~/.local/share/gomu/urls
popup_timeout: 5s
volume: 100
emoji: true
emoji: false
fzf: false
emoji:
playlist: 
file: 
loop: ﯩ
noloop: 
# vi:ft=yaml
```
## Fzf
@ -81,28 +91,51 @@ edit this line `fzf: false` to change it into `true` in `~/.config/gomu/config`.
## Keybindings
Each panel has it's own additional keybinding. To view the available keybinding for the specific panel use `?`
| Key | Description |
|:----------------|-----------------------:|
| j | down |
| k | up |
| tab | change panel |
| space | toggle play/pause |
| esc | close popup |
| n | skip |
| q | quit |
| l (lowercase L) | add song to queue |
| L | add playlist to queue |
| h | close node in playlist |
| d | remove from queue |
| D | delete playlist |
| + | volume up |
| - | volume down |
| y | search youtube |
| Y | download audio |
| a | create playlist |
| ? | toggle help |
| Key (General) | Description |
|:----------------|--------------------------------:|
| tab | change panel |
| space | toggle play/pause |
| esc | close popup |
| n | skip |
| q | quit |
| + | volume up |
| - | volume down |
| f | forward 10 seconds |
| F | forward 60 seconds |
| b | rewind 10 seconds |
| B | rewind 60 seconds |
| ? | toggle help |
| Key (Playlist) | Description |
|:----------------|--------------------------------:|
| j | down |
| k | up |
| h | close node in playlist |
| a | create playlist |
| l (lowercase L) | add song to queue |
| L | add playlist to queue |
| d | delete file from filesystemd |
| D | delete playlist from filesystem |
| Y | download audio |
| r | refresh |
| R | rename |
| y | yank file |
| p | paste file |
| / | find in playlist |
| s | search audio from youtube |
| Key (Queue) | Description |
|:----------------|--------------------------------:|
| j | down |
| k | up |
| l (lowercase L) | play selected song |
| d | remove from queue |
| D | delete playlist |
| z | toggle loop |
| s | shuffle |
| f | find in queue |
## Project Background
I just wanted to implement my own music player with a programming language i'm currently learning ([Go](https://golang.org/)). Gomu might not be stable as it in constant development. For now, it can fulfill basic music player functions such as:
@ -111,5 +144,6 @@ I just wanted to implement my own music player with a programming language i'm c
- skip
- play
- pause
- forward and rewind
Seeking and more advanced stuff has not yet been implemented; feel free to contribute :)

View File

@ -68,7 +68,7 @@ func (c Command) defineCommands() {
popupId := "youtube-search-input-popup"
input := newInputPopup(popupId, " Youtube Search ", "search: ")
input := newInputPopup(popupId, " Youtube Search ", "search: ", "")
// quick hack to change the autocomplete text color
tview.Styles.PrimitiveBackgroundColor = tcell.ColorBlack
@ -163,8 +163,9 @@ func (c Command) defineCommands() {
audioFile := gomu.playlist.getCurrentFile()
currNode := gomu.playlist.GetCurrentNode()
if gomu.pages.HasPage("download-popup") {
gomu.pages.RemovePage("download-popup")
if gomu.pages.HasPage("download-input-popup") {
gomu.pages.RemovePage("download-input-popup")
gomu.popups.pop()
return
}
// this ensures it downloads to
@ -280,27 +281,20 @@ func (c Command) defineCommands() {
})
c.define("play_selected", func() {
a, err := gomu.queue.deleteItem(gomu.queue.GetCurrentItem())
if err != nil {
logError(err)
}
if gomu.queue.GetItemCount() != 0 && gomu.queue.GetCurrentItem() != -1 {
a, err := gomu.queue.deleteItem(gomu.queue.GetCurrentItem())
if err != nil {
logError(err)
}
gomu.queue.pushFront(a)
gomu.player.skip()
gomu.queue.pushFront(a)
gomu.player.skip()
}
})
c.define("toggle_loop", func() {
isLoop := gomu.player.toggleLoop()
var msg string
if isLoop {
msg = "Looping current queue"
} else {
msg = "Stopped looping current queue"
}
defaultTimedPopup(" Loop ", msg)
gomu.queue.isLoop = gomu.player.toggleLoop()
gomu.queue.updateTitle()
})
c.define("shuffle_queue", func() {
@ -399,4 +393,65 @@ func (c Command) defineCommands() {
}
})
})
c.define("forward", func() {
if gomu.player.isRunning && !gomu.player.ctrl.Paused {
position := gomu.playingBar._progress + 10
if position < gomu.playingBar.full {
gomu.player.seek(position)
gomu.playingBar._progress = position - 1
}
}
})
c.define("rewind", func() {
if gomu.player.isRunning && !gomu.player.ctrl.Paused {
position := gomu.playingBar._progress - 10
if position-1 > 0 {
gomu.player.seek(position)
gomu.playingBar._progress = position - 1
} else {
gomu.player.seek(0)
gomu.playingBar._progress = 0
}
}
})
c.define("forward_fast", func() {
if gomu.player.isRunning && !gomu.player.ctrl.Paused {
position := gomu.playingBar._progress + 60
if position < gomu.playingBar.full {
gomu.player.seek(position)
gomu.playingBar._progress = position - 1
}
}
})
c.define("rewind_fast", func() {
if gomu.player.isRunning && !gomu.player.ctrl.Paused {
position := gomu.playingBar._progress - 60
if position-1 > 0 {
gomu.player.seek(position)
gomu.playingBar._progress = position - 1
} else {
gomu.player.seek(0)
gomu.playingBar._progress = 0
}
}
})
c.define("yank", func() {
err := gomu.playlist.yank()
if err != nil {
logError(err)
}
})
c.define("paste", func() {
err := gomu.playlist.paste()
if err != nil {
logError(err)
}
})
}

4
go.mod
View File

@ -5,7 +5,7 @@ go 1.14
require (
github.com/faiface/beep v1.0.2
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/gdamore/tcell/v2 v2.0.1-0.20201017141208-acf90d56d591
github.com/gdamore/tcell/v2 v2.1.0
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
github.com/hajimehoshi/go-mp3 v0.3.1 // indirect
github.com/hajimehoshi/oto v0.7.1 // indirect
@ -15,7 +15,7 @@ require (
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/pelletier/go-toml v1.8.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/tview v0.0.0-20210117162420-745e4ceeb711
github.com/rivo/tview v0.0.0-20210125085121-dbc1f32bb1d0
github.com/sahilm/fuzzy v0.1.0
github.com/spf13/afero v1.5.1 // indirect
github.com/spf13/cast v1.3.1 // indirect

4
go.sum
View File

@ -51,6 +51,8 @@ github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU=
github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0=
github.com/gdamore/tcell/v2 v2.0.1-0.20201017141208-acf90d56d591 h1:0WWUDZ1oxq7NxVyGo8M3KI5jbkiwNAdZFFzAdC68up4=
github.com/gdamore/tcell/v2 v2.0.1-0.20201017141208-acf90d56d591/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
github.com/gdamore/tcell/v2 v2.1.0 h1:UnSmozHgBkQi2PGsFr+rpdXuAPRRucMegpQp3Z3kDro=
github.com/gdamore/tcell/v2 v2.1.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@ -202,6 +204,8 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rivo/tview v0.0.0-20210117162420-745e4ceeb711 h1:9xC0sXenoeJK2jP8LK24H4FwcCcPwK8ZNCxgURhn52c=
github.com/rivo/tview v0.0.0-20210117162420-745e4ceeb711/go.mod h1:1QW7hX7RQzOqyGgx8O64bRPQBrFtPflioPPX5gFPV3A=
github.com/rivo/tview v0.0.0-20210125085121-dbc1f32bb1d0 h1:WCfp+Jq9Mx156zIf9X6Frd6F19rf7wIRlm54UPxUfcU=
github.com/rivo/tview v0.0.0-20210125085121-dbc1f32bb1d0/go.mod h1:1QW7hX7RQzOqyGgx8O64bRPQBrFtPflioPPX5gFPV3A=
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=

24
gomu.go
View File

@ -90,6 +90,24 @@ func (g *Gomu) cyclePanels() Panel {
return first
}
func (g *Gomu) cyclePanels2() Panel {
first := g.panels[0]
second := g.panels[1]
if first.HasFocus() {
g.setFocusPanel(second)
g.prevPanel = second
return second
} else if second.HasFocus() {
g.setFocusPanel(first)
g.prevPanel = first
return first
} else {
g.setFocusPanel(first)
g.prevPanel = first
return first
}
}
// Changes title and border color when focusing panel
// and changes color of the previous panel as well
func (g *Gomu) setFocusPanel(panel Panel) {
@ -102,7 +120,9 @@ func (g *Gomu) setFocusPanel(panel Panel) {
return
}
g.setUnfocusPanel(g.prevPanel)
if g.prevPanel != panel {
g.setUnfocusPanel(g.prevPanel)
}
}
// Safely write the IsSuspend state, IsSuspend is used to indicate if we
@ -141,7 +161,7 @@ func (g *Gomu) setUnfocusPanel(panel Panel) {
func (g *Gomu) quit(args Args) error {
if !*args.empty {
err := gomu.queue.saveQueue()
err := gomu.queue.saveQueue(true)
if err != nil {
return tracerr.Wrap(err)
}

View File

@ -30,7 +30,6 @@ func setupLog() {
log.Fatalf("Error opening file %s", logFile)
}
log.SetOutput(file)
log.SetFlags(log.Ldate | log.Ltime | log.Llongfile)
}

View File

@ -24,13 +24,16 @@ type Player struct {
done chan bool
// to control the _volume internally
_volume *effects.Volume
ctrl *beep.Ctrl
volume float64
resampler *beep.Resampler
position time.Duration
length time.Duration
currentSong *AudioFile
_volume *effects.Volume
ctrl *beep.Ctrl
volume float64
resampler *beep.Resampler
position time.Duration
length time.Duration
currentSong *AudioFile
streamSeekCloser beep.StreamSeekCloser
// is used to send progress
i int
}
func newPlayer() *Player {
@ -58,21 +61,24 @@ func (p *Player) run(currSong *AudioFile) error {
defer f.Close()
streamer, format, err := mp3.Decode(f)
stream, format, err := mp3.Decode(f)
p.streamSeekCloser = stream
p.format = &format
if err != nil {
return tracerr.Wrap(err)
}
defer streamer.Close()
defer stream.Close()
// song duration
p.length = format.SampleRate.D(streamer.Len())
p.length = p.format.SampleRate.D(p.streamSeekCloser.Len())
if !p.hasInit {
err := speaker.
Init(format.SampleRate, format.SampleRate.N(time.Second/10))
Init(p.format.SampleRate, p.format.SampleRate.N(time.Second/10))
if err != nil {
return tracerr.Wrap(err)
@ -81,7 +87,6 @@ func (p *Player) run(currSong *AudioFile) error {
p.hasInit = true
}
p.format = &format
p.currentSong = currSong
popupMessage := fmt.Sprintf("%s\n\n[ %s ]",
@ -92,7 +97,7 @@ func (p *Player) run(currSong *AudioFile) error {
done := make(chan bool, 1)
p.done = done
sstreamer := beep.Seq(streamer, beep.Callback(func() {
sstreamer := beep.Seq(p.streamSeekCloser, beep.Callback(func() {
done <- true
}))
@ -118,18 +123,9 @@ func (p *Player) run(currSong *AudioFile) error {
p._volume = volume
speaker.Play(p._volume)
position := func() time.Duration {
return format.SampleRate.D(streamer.Position())
}
p.position = position()
p.position = p.getPosition()
p.isRunning = true
if p.isLoop {
gomu.queue.enqueue(currSong)
gomu.app.Draw()
}
gomu.playingBar.newProgress(currSong.name, int(p.length.Seconds()))
go func() {
@ -139,13 +135,18 @@ func (p *Player) run(currSong *AudioFile) error {
}()
// is used to send progress
i := 0
p.i = 0
next:
for {
select {
case <-done:
if p.isLoop {
gomu.queue.enqueue(currSong)
gomu.app.Draw()
}
close(done)
p.position = 0
p.isRunning = false
@ -173,14 +174,14 @@ next:
continue
}
i++
p.i++
gomu.playingBar.progress <- 1
speaker.Lock()
p.position = position()
p.position = p.getPosition()
speaker.Unlock()
if i > gomu.playingBar.full {
if p.i > gomu.playingBar.full {
break next
}
@ -248,6 +249,7 @@ func (p *Player) skip() {
}
p.ctrl.Streamer = nil
p.streamSeekCloser.Close()
p.done <- true
}
@ -290,3 +292,16 @@ func volToHuman(volume float64) int {
func absVolume(volume int) float64 {
return (float64(volume) - 100) / 10
}
func (p *Player) getPosition() time.Duration {
return p.format.SampleRate.D(p.streamSeekCloser.Position())
}
//seek is the function to move forward and rewind
func (p *Player) seek(pos int) error {
speaker.Lock()
defer speaker.Unlock()
err := p.streamSeekCloser.Seek(pos * int(p.format.SampleRate))
p.i = pos - 1
return err
}

View File

@ -21,7 +21,7 @@ import (
"github.com/ztrue/tracerr"
)
// Playlist and mp3 files are represented with this struct
// AudioFile is representing directories and mp3 files
// if isAudioFile equals to false it is a directory
type AudioFile struct {
name string
@ -43,6 +43,11 @@ type Playlist struct {
done chan struct{}
}
var (
yankFile *AudioFile
isYanked bool
)
func (p *Playlist) help() []string {
return []string{
@ -58,6 +63,8 @@ func (p *Playlist) help() []string {
"y query audio from youtube and download",
"r refresh",
"R rename",
"y yank file",
"p paste file",
"/ find in playlist",
}
@ -146,6 +153,8 @@ func newPlaylist(args Args) *Playlist {
'h': "close_node",
'r': "refresh",
'R': "rename",
'y': "yank",
'p': "paste",
'/': "playlist_search",
}
@ -192,6 +201,7 @@ func (p *Playlist) deleteSong(audioFile *AudioFile) (err error) {
return
}
audioName := getName(audioFile.path)
err := os.Remove(audioFile.path)
if err != nil {
@ -204,8 +214,18 @@ func (p *Playlist) deleteSong(audioFile *AudioFile) (err error) {
defaultTimedPopup(" Success ",
audioFile.name+"\nhas been deleted successfully")
p.refresh()
//Here we remove the song from queue
songPaths := gomu.queue.getItems()
if audioName == getName(gomu.player.currentSong.name) {
gomu.player.skip()
}
for i, songPath := range songPaths {
if strings.Contains(songPath, audioName) {
gomu.queue.deleteItem(i)
}
}
}
})
@ -269,7 +289,11 @@ func (p *Playlist) addAllToQueue(root *tview.TreeNode) {
for _, v := range childrens {
currNode := v.GetReference().(*AudioFile)
gomu.queue.enqueue(currNode)
if currNode.isAudioFile {
if currNode != gomu.player.currentSong {
gomu.queue.enqueue(currNode)
}
}
}
}
@ -285,7 +309,7 @@ func (p *Playlist) refresh() {
populate(root, root.GetReference().(*AudioFile).path)
root.Walk(func(node, parent *tview.TreeNode) bool {
root.Walk(func(node, _ *tview.TreeNode) bool {
// to preserve previously highlighted node
if node.GetText() == prevFileName {
@ -504,6 +528,11 @@ func (p *Playlist) rename(newName string) error {
return tracerr.Wrap(err)
}
audio.path = newPath
gomu.queue.saveQueue(false)
gomu.queue.clearQueue()
gomu.queue.loadQueue()
return nil
}
@ -592,18 +621,15 @@ func ytdl(url string, selPlaylist *tview.TreeNode) error {
return tracerr.Wrap(err)
}
dir := viper.GetString("general.music_dir")
selAudioFile := selPlaylist.GetReference().(*AudioFile)
selPlaylistName := selAudioFile.name
dir := selAudioFile.path
defaultTimedPopup(" Ytdl ", "Downloading")
// specify the output path for ytdl
outputDir := fmt.Sprintf(
"%s/%s/%%(title)s.%%(ext)s",
dir,
selPlaylistName)
"%s/%%(title)s.%%(ext)s",
dir)
args := []string{
"--extract-audio",
@ -611,6 +637,8 @@ func ytdl(url string, selPlaylist *tview.TreeNode) error {
"mp3",
"--output",
outputDir,
// "--cookies",
// "~/Downloads/youtube.com_cookies.txt",
url,
}
@ -632,7 +660,7 @@ func ytdl(url string, selPlaylist *tview.TreeNode) error {
return tracerr.Wrap(err)
}
playlistPath := path.Join(expandTilde(dir), selPlaylistName)
playlistPath := dir
audioPath := extractFilePath(stdout.Bytes(), playlistPath)
err = appendFile(expandTilde(viper.GetString("general.history_path")), url+"\n")
@ -645,10 +673,9 @@ func ytdl(url string, selPlaylist *tview.TreeNode) error {
return tracerr.Wrap(err)
}
downloadFinishedMessage := fmt.Sprintf("Finished downloading\n%s",
getName(audioPath))
downloadFinishedMessage := fmt.Sprintf("Finished downloading\n%s", getName(audioPath))
defaultTimedPopup(" Ytdl ", downloadFinishedMessage)
gomu.app.Draw()
return nil
}
@ -664,19 +691,21 @@ func populate(root *tview.TreeNode, rootPath string) error {
for _, file := range files {
path := filepath.Join(rootPath, file.Name())
f, err := os.Open(path)
path, err := filepath.EvalSymlinks(filepath.Join(rootPath, file.Name()))
if err != nil {
continue
}
defer f.Close()
songName := getName(file.Name())
child := tview.NewTreeNode(songName)
if !file.IsDir() {
if file.Mode().IsRegular() {
f, err := os.Open(path)
if err != nil {
continue
}
defer f.Close()
filetype, err := getFileContentType(f)
@ -689,17 +718,10 @@ func populate(root *tview.TreeNode, rootPath string) error {
continue
}
audioLength, err := getLength(path)
if err != nil {
continue
}
audioFile := &AudioFile{
name: songName,
path: path,
isAudioFile: true,
length: audioLength,
node: child,
parent: root,
}
@ -716,13 +738,14 @@ func populate(root *tview.TreeNode, rootPath string) error {
}
if file.IsDir() {
if file.IsDir() || file.Mode()&os.ModeSymlink != 0 {
audioFile := &AudioFile{
name: songName,
path: path,
node: child,
parent: root,
name: songName,
path: path,
isAudioFile: false,
node: child,
parent: root,
}
displayText := songName
@ -743,3 +766,81 @@ func populate(root *tview.TreeNode, rootPath string) error {
return nil
}
func (p *Playlist) yank() error {
yankFile = p.getCurrentFile()
if yankFile == nil {
isYanked = false
defaultTimedPopup(" Error! ", "No file has been yanked.")
return nil
}
if yankFile.node == p.GetRoot() {
isYanked = false
defaultTimedPopup(" Error! ", "Please don't yank the root directory.")
return nil
}
isYanked = true
defaultTimedPopup(" Success ", yankFile.name+"\n has been yanked successfully.")
return nil
}
func (p *Playlist) paste() error {
if isYanked {
isYanked = false
oldPathDir, oldPathFileName := filepath.Split(yankFile.path)
pasteFile := p.getCurrentFile()
if pasteFile.isAudioFile {
newPathDir, _ := filepath.Split(pasteFile.path)
if oldPathDir == newPathDir {
return nil
} else {
newPathFull := filepath.Join(newPathDir, oldPathFileName)
err := os.Rename(yankFile.path, newPathFull)
if err != nil {
defaultTimedPopup(" Error ", yankFile.name+"\n has not been pasted.")
return tracerr.Wrap(err)
}
defaultTimedPopup(" Success ", yankFile.name+"\n has been pasted to\n"+pasteFile.name)
}
} else {
newPathDir := pasteFile.path
if oldPathDir == newPathDir {
return nil
} else {
newPathFull := filepath.Join(newPathDir, oldPathFileName)
err := os.Rename(yankFile.path, newPathFull)
if err != nil {
defaultTimedPopup(" Error ", yankFile.name+"\n has not been pasted.")
return tracerr.Wrap(err)
}
defaultTimedPopup(" Success ", yankFile.name+"\n has been pasted to\n"+pasteFile.name)
}
}
p.refresh()
gomu.queue.updateQueueNames()
}
return nil
}
//populateAudioLength is the most time consuming part of startup,
//so here we initialize it separately
func populateAudioLength(root *tview.TreeNode) error {
root.Walk(func(node *tview.TreeNode, _ *tview.TreeNode) bool {
audioFile := node.GetReference().(*AudioFile)
if audioFile.isAudioFile {
audioLength, err := getLength(audioFile.path)
if err != nil {
logError(err)
return false
}
audioFile.length = audioLength
}
return true
})
gomu.queue.updateTitle()
return nil
}

View File

@ -7,7 +7,7 @@ import (
"regexp"
"strings"
"time"
"unicode/utf8"
"unicode/utf8"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
@ -99,13 +99,13 @@ func confirmationPopup(
modal.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey {
switch e.Rune() {
case 'h':
return tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModNone)
return tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModNone)
case 'j':
return tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModNone)
return tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModNone)
case 'k':
return tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModNone)
return tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModNone)
case 'l':
return tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModNone)
return tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModNone)
}
return e
})
@ -163,15 +163,15 @@ func timedPopup(
box := tview.NewFrame(textView).SetBorders(1, 0, 0, 0, 0, 0)
box.SetTitle(title).SetBorder(true).SetBackgroundColor(gomu.colors.popup)
popupId := fmt.Sprintf("%s %d", "timeout-popup", popupCounter)
popupID := fmt.Sprintf("%s %d", "timeout-popup", popupCounter)
popupCounter++
gomu.pages.AddPage(popupId, topRight(box, width, height), true, true)
gomu.pages.AddPage(popupID, topRight(box, width, height), true, true)
gomu.app.SetFocus(gomu.prevPanel.(tview.Primitive))
go func() {
time.Sleep(timeout)
gomu.pages.RemovePage(popupId)
gomu.pages.RemovePage(popupID)
gomu.app.Draw()
// timed popup shouldn't get focused
@ -225,6 +225,10 @@ func helpPopup(panel Panel) {
"q quit",
"+ volume up",
"- volume down",
"f forward 10 seconds",
"F forward 60 seconds",
"b rewind 10 seconds",
"B rewind 60 seconds",
"? toggle help",
}
@ -235,7 +239,7 @@ func helpPopup(panel Panel) {
SetSelectedTextColor(gomu.colors.accent)
for _, v := range append(helpText, genHelp...) {
list.AddItem(" " + v, "", 0, nil)
list.AddItem(" "+v, "", 0, nil)
}
prev := func() {
@ -279,8 +283,8 @@ func downloadMusicPopup(selPlaylist *tview.TreeNode) {
re := regexp.MustCompile(`^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$`)
popupId := "download-input-popup"
input := newInputPopup(popupId, " Download ", "Url: ")
popupID := "download-input-popup"
input := newInputPopup(popupID, " Download ", "Url: ", "")
input.SetDoneFunc(func(key tcell.Key) {
@ -316,8 +320,8 @@ func downloadMusicPopup(selPlaylist *tview.TreeNode) {
// Input popup that takes the name of directory to be created
func createPlaylistPopup() {
popupId := "mkdir-input-popup"
input := newInputPopup(popupId, " New Playlist ", "Enter playlist name: ")
popupID := "mkdir-input-popup"
input := newInputPopup(popupID, " New Playlist ", "Enter playlist name: ", "")
input.SetDoneFunc(func(key tcell.Key) {
@ -389,26 +393,25 @@ func searchPopup(stringsToMatch []string, handler func(selected string)) {
for _, match := range matches {
var text strings.Builder
matchrune := [] rune(match.Str)
matchruneIndexes := match.MatchedIndexes
for i:=0; i < len(match.MatchedIndexes); i++{
matchruneIndexes[i] = utf8.RuneCountInString(match.Str[0:match.MatchedIndexes[i]])
}
for i :=0; i < len(matchrune); i++ {
if contains(i, matchruneIndexes) {
textwithcolor := fmt.Sprintf(highlight, matchrune[i])
for _,j := range textwithcolor {
text.WriteRune(j)
}
} else{
text.WriteRune(matchrune[i])
}
}
matchrune := []rune(match.Str)
matchruneIndexes := match.MatchedIndexes
for i := 0; i < len(match.MatchedIndexes); i++ {
matchruneIndexes[i] = utf8.RuneCountInString(match.Str[0:match.MatchedIndexes[i]])
}
for i := 0; i < len(matchrune); i++ {
if contains(i, matchruneIndexes) {
textwithcolor := fmt.Sprintf(highlight, matchrune[i])
for _, j := range textwithcolor {
text.WriteRune(j)
}
} else {
text.WriteRune(matchrune[i])
}
}
list.AddItem(text.String(), match.Str, 0, nil)
}
})
input.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey {
switch e.Key() {
@ -462,7 +465,7 @@ func searchPopup(stringsToMatch []string, handler func(selected string)) {
}
// Creates new popup widget with default settings
func newInputPopup(popupId, title, label string) *tview.InputField {
func newInputPopup(popupID, title, label string, text string) *tview.InputField {
inputField := tview.NewInputField().
SetLabel(label).
@ -476,8 +479,10 @@ func newInputPopup(popupId, title, label string) *tview.InputField {
SetBorder(true).
SetBorderPadding(1, 0, 2, 2)
inputField.SetText(text)
gomu.pages.
AddPage(popupId, center(inputField, 60, 5), true, true)
AddPage(popupID, center(inputField, 60, 5), true, true)
gomu.popups.push(inputField)
@ -486,8 +491,8 @@ func newInputPopup(popupId, title, label string) *tview.InputField {
func renamePopup(node *AudioFile) {
popupId := "rename-input-popup"
input := newInputPopup(popupId, " Rename ", "New name: ")
popupID := "rename-input-popup"
input := newInputPopup(popupID, " Rename ", "New name: ", node.name)
input.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey {
switch e.Key() {
@ -501,12 +506,26 @@ func renamePopup(node *AudioFile) {
defaultTimedPopup(" Error ", err.Error())
logError(err)
}
gomu.pages.RemovePage(popupId)
gomu.pages.RemovePage(popupID)
gomu.popups.pop()
gomu.playlist.refresh()
// gomu.queue.saveQueue()
// gomu.queue.clearQueue()
// gomu.queue.loadQueue()
gomu.queue.updateQueueNames()
gomu.setFocusPanel(gomu.playlist)
gomu.prevPanel = gomu.playlist
// gomu.playlist.setHighlight(node.node)
root := gomu.playlist.GetRoot()
root.Walk(func(node, _ *tview.TreeNode) bool {
if strings.Contains(node.GetText(), newName) {
gomu.playlist.setHighlight(node)
}
return true
})
case tcell.KeyEsc:
gomu.pages.RemovePage(popupId)
gomu.pages.RemovePage(popupID)
gomu.popups.pop()
}

View File

@ -16,6 +16,7 @@ import (
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
"github.com/spf13/viper"
"github.com/ztrue/tracerr"
)
@ -95,8 +96,25 @@ func (q *Queue) updateTitle() string {
count = "song"
}
title := fmt.Sprintf("─ Queue ───┤ %d %s | %s ├",
len(q.items), count, fmtTime)
var loop string
isEmoji := viper.GetBool("general.emoji")
if q.isLoop {
if isEmoji {
loop = viper.GetString("emoji.loop")
} else {
loop = "Loop"
}
} else {
if isEmoji {
loop = viper.GetString("emoji.noloop")
} else {
loop = "No loop"
}
}
title := fmt.Sprintf("─ Queue ───┤ %d %s | %s | %s ├",
len(q.items), count, fmtTime, loop)
q.SetTitle(title)
@ -136,20 +154,27 @@ func (q *Queue) dequeue() (*AudioFile, error) {
// Add item to the list and returns the length of the queue
func (q *Queue) enqueue(audioFile *AudioFile) (int, error) {
if !gomu.player.isRunning && "false" == os.Getenv("TEST") {
//this is to fix the problem bulk_add when paused
if !gomu.player.hasInit {
if !gomu.player.isRunning && os.Getenv("TEST") == "false" {
gomu.player.isRunning = true
gomu.player.isRunning = true
go func() {
go func() {
if err := gomu.player.run(audioFile); err != nil {
logError(err)
}
if err := gomu.player.run(audioFile); err != nil {
logError(err)
}
}()
}()
return q.GetItemCount(), nil
}
}
if !audioFile.isAudioFile {
return q.GetItemCount(), nil
}
q.items = append(q.items, audioFile)
@ -186,11 +211,25 @@ func (q *Queue) getItems() []string {
}
// Save the current queue
func (q *Queue) saveQueue() error {
func (q *Queue) saveQueue(isQuit bool) error {
songPaths := q.getItems()
var content strings.Builder
if gomu.player.hasInit && isQuit {
currentSongPath := gomu.player.currentSong.path
currentSongInQueue := false
for _, songPath := range songPaths {
if getName(songPath) == getName(currentSongPath) {
currentSongInQueue = true
}
}
if !currentSongInQueue {
hashed := sha1Hex(getName(currentSongPath))
content.WriteString(hashed + "\n")
}
}
for _, songPath := range songPaths {
// hashed song name is easier to search through
hashed := sha1Hex(getName(songPath))
@ -360,7 +399,7 @@ func (q *Queue) shuffle() {
q.AddItem(queueText, v.path, 0, nil)
}
q.updateTitle()
// q.updateTitle()
}
@ -421,3 +460,11 @@ func sha1Hex(input string) string {
h.Write([]byte(input))
return hex.EncodeToString(h.Sum(nil))
}
//Modify the title of songs in queue
func (q *Queue) updateQueueNames() error {
q.saveQueue(false)
q.clearQueue()
q.loadQueue()
return nil
}

View File

@ -18,7 +18,7 @@ import (
"github.com/spf13/viper"
)
// Created so we can keep track of childrens in slices
// Panel is used to keep track of childrens in slices
type Panel interface {
HasFocus() bool
SetBorderColor(color tcell.Color) *tview.Box
@ -29,9 +29,9 @@ type Panel interface {
}
const (
CONFIG_PATH = ".config/gomu/config"
HISTORY_PATH = "~/.local/share/gomu/urls"
MUSIC_PATH = "~/music"
configPath = ".config/gomu/config"
historyPath = "~/.local/share/gomu/urls"
musicPath = "~/music"
)
// Reads config file and sets the options
@ -43,6 +43,7 @@ general:
confirm_bulk_add: true
confirm_on_exit: true
load_prev_queue: true
queue_loop: true
# change this to directory that contains mp3 files
music_dir: ~/music
# url history of downloaded audio will be saved here
@ -60,7 +61,7 @@ general:
# if you experiencing error using this invidious instance, you can change it
# to another instance from this list:
# https://github.com/iv-org/documentation/blob/master/Invidious-Instances.md
invidious_instance: "https://invidious.namazso.eu"
invidious_instance: "https://vid.puffyan.us"
# not all colors can be reproducible in terminal
# changing hex colors may or may not produce expected result
@ -79,7 +80,9 @@ color:
emoji:
playlist:
file:
loop:
noloop:
# vi:ft=yaml
`
@ -91,7 +94,7 @@ emoji:
logError(err)
}
defaultPath := path.Join(home, CONFIG_PATH)
defaultPath := path.Join(home, configPath)
if err != nil {
logError(err)
@ -103,19 +106,18 @@ emoji:
viper.AddConfigPath("$HOME/.config/gomu")
// General config
viper.SetDefault("general.music_dir", MUSIC_PATH)
viper.SetDefault("general.history_path", HISTORY_PATH)
viper.SetDefault("general.music_dir", musicPath)
viper.SetDefault("general.history_path", historyPath)
viper.SetDefault("general.confirm_on_exit", true)
viper.SetDefault("general.confirm_bulk_add", true)
viper.SetDefault("general.popup_timeout", "5s")
viper.SetDefault("general.volume", 100)
viper.SetDefault("general.load_prev_queue", true)
viper.SetDefault("general.use_emoji", false)
viper.SetDefault("general.invidious_instance", "https://invidious.namazso.eu")
viper.SetDefault("general.invidious_instance", "https://vid.puffyan.us")
if err := viper.ReadInConfig(); err != nil {
// creates gomu config dir if does not exist
if _, err := os.Stat(defaultPath); err != nil {
if err := os.MkdirAll(home+"/.config/gomu", 0755); err != nil {
@ -146,9 +148,9 @@ type Args struct {
func getArgs() Args {
ar := Args{
config: flag.String("config", CONFIG_PATH, "Specify config file"),
config: flag.String("config", configPath, "Specify config file"),
empty: flag.Bool("empty", false, "Open gomu with empty queue. Does not override previous queue"),
music: flag.String("music", MUSIC_PATH, "Specify music directory"),
music: flag.String("music", musicPath, "Specify music directory"),
version: flag.Bool("version", false, "Print gomu version"),
}
flag.Parse()
@ -199,6 +201,9 @@ func start(application *tview.Application, args Args) {
gomu.setFocusPanel(gomu.playlist)
gomu.prevPanel = gomu.playlist
gomu.player.isLoop = viper.GetBool("general.queue_loop")
gomu.queue.isLoop = gomu.player.isLoop
if !*args.empty && viper.GetBool("general.load_prev_queue") {
// load saved queue from previous session
if err := gomu.queue.loadQueue(); err != nil {
@ -207,14 +212,14 @@ func start(application *tview.Application, args Args) {
}
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGKILL)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigs
errMsg := fmt.Sprintf("Received %s. Exiting program", sig.String())
logError(errors.New(errMsg))
err := gomu.quit(args)
if err != nil {
logError(errors.New("Unable to quit program"))
logError(errors.New("unable to quit program"))
}
}()
@ -234,17 +239,23 @@ func start(application *tview.Application, args Args) {
if strings.Contains(popupName, "confirmation-") {
return e
}
gomu.cyclePanels()
gomu.cyclePanels2()
}
cmds := map[rune]string{
'q': "quit",
' ': "toggle_pause",
'+': "volume_up",
'=': "volume_up",
'-': "volume_down",
'_': "volume_down",
'n': "skip",
':': "command_search",
'?': "toggle_help",
'f': "forward",
'F': "forward_fast",
'b': "rewind",
'B': "rewind_fast",
}
for key, cmd := range cmds {
@ -268,8 +279,10 @@ func start(application *tview.Application, args Args) {
return false
})
go populateAudioLength(gomu.playlist.GetRoot())
// main loop
if err := application.SetRoot(gomu.pages, true).SetFocus(gomu.playlist).Run(); err != nil {
logError(err)
}
}

View File

@ -21,7 +21,6 @@ func logError(err error) {
log.Println(tracerr.Sprint(err))
}
// Formats duration to my desired output mm:ss
func fmtDuration(input time.Duration) string {