mirror of
https://github.com/issadarkthing/gomu.git
synced 2025-04-28 13:48:53 +08:00
Merge branch 'master' into youtube-search
This commit is contained in:
commit
f2f225a058
60
README.md
60
README.md
@ -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
|
- [youtube-dl](https://github.com/ytdl-org/youtube-dl) integration
|
||||||
- audio file management
|
- audio file management
|
||||||
- customizable
|
- customizable
|
||||||
|
- find music from youtube
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
If you are using ubuntu, you need to install alsa and required dependencies
|
If you are using ubuntu, you need to install alsa and required dependencies
|
||||||
@ -64,10 +65,19 @@ general:
|
|||||||
confirm_on_exit: true
|
confirm_on_exit: true
|
||||||
load_prev_queue: true
|
load_prev_queue: true
|
||||||
music_dir: ~/music
|
music_dir: ~/music
|
||||||
|
history_path: ~/.local/share/gomu/urls
|
||||||
popup_timeout: 5s
|
popup_timeout: 5s
|
||||||
volume: 100
|
volume: 100
|
||||||
emoji: true
|
emoji: false
|
||||||
fzf: false
|
fzf: false
|
||||||
|
|
||||||
|
emoji:
|
||||||
|
playlist:
|
||||||
|
file:
|
||||||
|
loop: ﯩ
|
||||||
|
noloop:
|
||||||
|
|
||||||
|
# vi:ft=yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
## Fzf
|
## Fzf
|
||||||
@ -81,28 +91,51 @@ edit this line `fzf: false` to change it into `true` in `~/.config/gomu/config`.
|
|||||||
## Keybindings
|
## Keybindings
|
||||||
Each panel has it's own additional keybinding. To view the available keybinding for the specific panel use `?`
|
Each panel has it's own additional keybinding. To view the available keybinding for the specific panel use `?`
|
||||||
|
|
||||||
| Key | Description |
|
| Key (General) | Description |
|
||||||
|:----------------|-----------------------:|
|
|:----------------|--------------------------------:|
|
||||||
| j | down |
|
|
||||||
| k | up |
|
|
||||||
| tab | change panel |
|
| tab | change panel |
|
||||||
| space | toggle play/pause |
|
| space | toggle play/pause |
|
||||||
| esc | close popup |
|
| esc | close popup |
|
||||||
| n | skip |
|
| n | skip |
|
||||||
| q | quit |
|
| 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 up |
|
||||||
| - | volume down |
|
| - | volume down |
|
||||||
| y | search youtube |
|
| f | forward 10 seconds |
|
||||||
| Y | download audio |
|
| F | forward 60 seconds |
|
||||||
| a | create playlist |
|
| b | rewind 10 seconds |
|
||||||
|
| B | rewind 60 seconds |
|
||||||
| ? | toggle help |
|
| ? | 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
|
## 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:
|
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
|
- skip
|
||||||
- play
|
- play
|
||||||
- pause
|
- pause
|
||||||
|
- forward and rewind
|
||||||
|
|
||||||
Seeking and more advanced stuff has not yet been implemented; feel free to contribute :)
|
Seeking and more advanced stuff has not yet been implemented; feel free to contribute :)
|
||||||
|
83
command.go
83
command.go
@ -68,7 +68,7 @@ func (c Command) defineCommands() {
|
|||||||
|
|
||||||
popupId := "youtube-search-input-popup"
|
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
|
// quick hack to change the autocomplete text color
|
||||||
tview.Styles.PrimitiveBackgroundColor = tcell.ColorBlack
|
tview.Styles.PrimitiveBackgroundColor = tcell.ColorBlack
|
||||||
@ -163,8 +163,9 @@ func (c Command) defineCommands() {
|
|||||||
|
|
||||||
audioFile := gomu.playlist.getCurrentFile()
|
audioFile := gomu.playlist.getCurrentFile()
|
||||||
currNode := gomu.playlist.GetCurrentNode()
|
currNode := gomu.playlist.GetCurrentNode()
|
||||||
if gomu.pages.HasPage("download-popup") {
|
if gomu.pages.HasPage("download-input-popup") {
|
||||||
gomu.pages.RemovePage("download-popup")
|
gomu.pages.RemovePage("download-input-popup")
|
||||||
|
gomu.popups.pop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// this ensures it downloads to
|
// this ensures it downloads to
|
||||||
@ -280,6 +281,7 @@ func (c Command) defineCommands() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
c.define("play_selected", func() {
|
c.define("play_selected", func() {
|
||||||
|
if gomu.queue.GetItemCount() != 0 && gomu.queue.GetCurrentItem() != -1 {
|
||||||
a, err := gomu.queue.deleteItem(gomu.queue.GetCurrentItem())
|
a, err := gomu.queue.deleteItem(gomu.queue.GetCurrentItem())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logError(err)
|
logError(err)
|
||||||
@ -287,20 +289,12 @@ func (c Command) defineCommands() {
|
|||||||
|
|
||||||
gomu.queue.pushFront(a)
|
gomu.queue.pushFront(a)
|
||||||
gomu.player.skip()
|
gomu.player.skip()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
c.define("toggle_loop", func() {
|
c.define("toggle_loop", func() {
|
||||||
|
gomu.queue.isLoop = gomu.player.toggleLoop()
|
||||||
isLoop := gomu.player.toggleLoop()
|
gomu.queue.updateTitle()
|
||||||
var msg string
|
|
||||||
|
|
||||||
if isLoop {
|
|
||||||
msg = "Looping current queue"
|
|
||||||
} else {
|
|
||||||
msg = "Stopped looping current queue"
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultTimedPopup(" Loop ", msg)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
c.define("shuffle_queue", func() {
|
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
4
go.mod
@ -5,7 +5,7 @@ go 1.14
|
|||||||
require (
|
require (
|
||||||
github.com/faiface/beep v1.0.2
|
github.com/faiface/beep v1.0.2
|
||||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
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/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
|
||||||
github.com/hajimehoshi/go-mp3 v0.3.1 // indirect
|
github.com/hajimehoshi/go-mp3 v0.3.1 // indirect
|
||||||
github.com/hajimehoshi/oto v0.7.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/mitchellh/mapstructure v1.4.1 // indirect
|
||||||
github.com/pelletier/go-toml v1.8.1 // indirect
|
github.com/pelletier/go-toml v1.8.1 // indirect
|
||||||
github.com/pkg/errors v0.9.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/sahilm/fuzzy v0.1.0
|
||||||
github.com/spf13/afero v1.5.1 // indirect
|
github.com/spf13/afero v1.5.1 // indirect
|
||||||
github.com/spf13/cast v1.3.1 // indirect
|
github.com/spf13/cast v1.3.1 // indirect
|
||||||
|
4
go.sum
4
go.sum
@ -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 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 h1:0WWUDZ1oxq7NxVyGo8M3KI5jbkiwNAdZFFzAdC68up4=
|
||||||
github.com/gdamore/tcell/v2 v2.0.1-0.20201017141208-acf90d56d591/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
|
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/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 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=
|
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/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 h1:9xC0sXenoeJK2jP8LK24H4FwcCcPwK8ZNCxgURhn52c=
|
||||||
github.com/rivo/tview v0.0.0-20210117162420-745e4ceeb711/go.mod h1:1QW7hX7RQzOqyGgx8O64bRPQBrFtPflioPPX5gFPV3A=
|
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 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
|
||||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
|
22
gomu.go
22
gomu.go
@ -90,6 +90,24 @@ func (g *Gomu) cyclePanels() Panel {
|
|||||||
return first
|
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
|
// Changes title and border color when focusing panel
|
||||||
// and changes color of the previous panel as well
|
// and changes color of the previous panel as well
|
||||||
func (g *Gomu) setFocusPanel(panel Panel) {
|
func (g *Gomu) setFocusPanel(panel Panel) {
|
||||||
@ -102,8 +120,10 @@ func (g *Gomu) setFocusPanel(panel Panel) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if g.prevPanel != panel {
|
||||||
g.setUnfocusPanel(g.prevPanel)
|
g.setUnfocusPanel(g.prevPanel)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Safely write the IsSuspend state, IsSuspend is used to indicate if we
|
// Safely write the IsSuspend state, IsSuspend is used to indicate if we
|
||||||
// are going to suspend the app. This should be used to widgets or
|
// are going to suspend the app. This should be used to widgets or
|
||||||
@ -141,7 +161,7 @@ func (g *Gomu) setUnfocusPanel(panel Panel) {
|
|||||||
func (g *Gomu) quit(args Args) error {
|
func (g *Gomu) quit(args Args) error {
|
||||||
|
|
||||||
if !*args.empty {
|
if !*args.empty {
|
||||||
err := gomu.queue.saveQueue()
|
err := gomu.queue.saveQueue(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return tracerr.Wrap(err)
|
return tracerr.Wrap(err)
|
||||||
}
|
}
|
||||||
|
1
main.go
1
main.go
@ -30,7 +30,6 @@ func setupLog() {
|
|||||||
log.Fatalf("Error opening file %s", logFile)
|
log.Fatalf("Error opening file %s", logFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
log.SetOutput(file)
|
log.SetOutput(file)
|
||||||
log.SetFlags(log.Ldate | log.Ltime | log.Llongfile)
|
log.SetFlags(log.Ldate | log.Ltime | log.Llongfile)
|
||||||
}
|
}
|
||||||
|
55
player.go
55
player.go
@ -31,6 +31,9 @@ type Player struct {
|
|||||||
position time.Duration
|
position time.Duration
|
||||||
length time.Duration
|
length time.Duration
|
||||||
currentSong *AudioFile
|
currentSong *AudioFile
|
||||||
|
streamSeekCloser beep.StreamSeekCloser
|
||||||
|
// is used to send progress
|
||||||
|
i int
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPlayer() *Player {
|
func newPlayer() *Player {
|
||||||
@ -58,21 +61,24 @@ func (p *Player) run(currSong *AudioFile) error {
|
|||||||
|
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
streamer, format, err := mp3.Decode(f)
|
stream, format, err := mp3.Decode(f)
|
||||||
|
|
||||||
|
p.streamSeekCloser = stream
|
||||||
|
p.format = &format
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return tracerr.Wrap(err)
|
return tracerr.Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer streamer.Close()
|
defer stream.Close()
|
||||||
|
|
||||||
// song duration
|
// song duration
|
||||||
p.length = format.SampleRate.D(streamer.Len())
|
p.length = p.format.SampleRate.D(p.streamSeekCloser.Len())
|
||||||
|
|
||||||
if !p.hasInit {
|
if !p.hasInit {
|
||||||
|
|
||||||
err := speaker.
|
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 {
|
if err != nil {
|
||||||
return tracerr.Wrap(err)
|
return tracerr.Wrap(err)
|
||||||
@ -81,7 +87,6 @@ func (p *Player) run(currSong *AudioFile) error {
|
|||||||
p.hasInit = true
|
p.hasInit = true
|
||||||
}
|
}
|
||||||
|
|
||||||
p.format = &format
|
|
||||||
p.currentSong = currSong
|
p.currentSong = currSong
|
||||||
|
|
||||||
popupMessage := fmt.Sprintf("%s\n\n[ %s ]",
|
popupMessage := fmt.Sprintf("%s\n\n[ %s ]",
|
||||||
@ -92,7 +97,7 @@ func (p *Player) run(currSong *AudioFile) error {
|
|||||||
done := make(chan bool, 1)
|
done := make(chan bool, 1)
|
||||||
p.done = done
|
p.done = done
|
||||||
|
|
||||||
sstreamer := beep.Seq(streamer, beep.Callback(func() {
|
sstreamer := beep.Seq(p.streamSeekCloser, beep.Callback(func() {
|
||||||
done <- true
|
done <- true
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@ -118,18 +123,9 @@ func (p *Player) run(currSong *AudioFile) error {
|
|||||||
p._volume = volume
|
p._volume = volume
|
||||||
speaker.Play(p._volume)
|
speaker.Play(p._volume)
|
||||||
|
|
||||||
position := func() time.Duration {
|
p.position = p.getPosition()
|
||||||
return format.SampleRate.D(streamer.Position())
|
|
||||||
}
|
|
||||||
|
|
||||||
p.position = position()
|
|
||||||
p.isRunning = true
|
p.isRunning = true
|
||||||
|
|
||||||
if p.isLoop {
|
|
||||||
gomu.queue.enqueue(currSong)
|
|
||||||
gomu.app.Draw()
|
|
||||||
}
|
|
||||||
|
|
||||||
gomu.playingBar.newProgress(currSong.name, int(p.length.Seconds()))
|
gomu.playingBar.newProgress(currSong.name, int(p.length.Seconds()))
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@ -139,13 +135,18 @@ func (p *Player) run(currSong *AudioFile) error {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// is used to send progress
|
// is used to send progress
|
||||||
i := 0
|
p.i = 0
|
||||||
|
|
||||||
next:
|
next:
|
||||||
for {
|
for {
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-done:
|
case <-done:
|
||||||
|
if p.isLoop {
|
||||||
|
gomu.queue.enqueue(currSong)
|
||||||
|
gomu.app.Draw()
|
||||||
|
}
|
||||||
|
|
||||||
close(done)
|
close(done)
|
||||||
p.position = 0
|
p.position = 0
|
||||||
p.isRunning = false
|
p.isRunning = false
|
||||||
@ -173,14 +174,14 @@ next:
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
i++
|
p.i++
|
||||||
gomu.playingBar.progress <- 1
|
gomu.playingBar.progress <- 1
|
||||||
|
|
||||||
speaker.Lock()
|
speaker.Lock()
|
||||||
p.position = position()
|
p.position = p.getPosition()
|
||||||
speaker.Unlock()
|
speaker.Unlock()
|
||||||
|
|
||||||
if i > gomu.playingBar.full {
|
if p.i > gomu.playingBar.full {
|
||||||
break next
|
break next
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,6 +249,7 @@ func (p *Player) skip() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p.ctrl.Streamer = nil
|
p.ctrl.Streamer = nil
|
||||||
|
p.streamSeekCloser.Close()
|
||||||
p.done <- true
|
p.done <- true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -290,3 +292,16 @@ func volToHuman(volume float64) int {
|
|||||||
func absVolume(volume int) float64 {
|
func absVolume(volume int) float64 {
|
||||||
return (float64(volume) - 100) / 10
|
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
|
||||||
|
}
|
||||||
|
155
playlist.go
155
playlist.go
@ -21,7 +21,7 @@ import (
|
|||||||
"github.com/ztrue/tracerr"
|
"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
|
// if isAudioFile equals to false it is a directory
|
||||||
type AudioFile struct {
|
type AudioFile struct {
|
||||||
name string
|
name string
|
||||||
@ -43,6 +43,11 @@ type Playlist struct {
|
|||||||
done chan struct{}
|
done chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
yankFile *AudioFile
|
||||||
|
isYanked bool
|
||||||
|
)
|
||||||
|
|
||||||
func (p *Playlist) help() []string {
|
func (p *Playlist) help() []string {
|
||||||
|
|
||||||
return []string{
|
return []string{
|
||||||
@ -58,6 +63,8 @@ func (p *Playlist) help() []string {
|
|||||||
"y query audio from youtube and download",
|
"y query audio from youtube and download",
|
||||||
"r refresh",
|
"r refresh",
|
||||||
"R rename",
|
"R rename",
|
||||||
|
"y yank file",
|
||||||
|
"p paste file",
|
||||||
"/ find in playlist",
|
"/ find in playlist",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,6 +153,8 @@ func newPlaylist(args Args) *Playlist {
|
|||||||
'h': "close_node",
|
'h': "close_node",
|
||||||
'r': "refresh",
|
'r': "refresh",
|
||||||
'R': "rename",
|
'R': "rename",
|
||||||
|
'y': "yank",
|
||||||
|
'p': "paste",
|
||||||
'/': "playlist_search",
|
'/': "playlist_search",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,6 +201,7 @@ func (p *Playlist) deleteSong(audioFile *AudioFile) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
audioName := getName(audioFile.path)
|
||||||
err := os.Remove(audioFile.path)
|
err := os.Remove(audioFile.path)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -204,8 +214,18 @@ func (p *Playlist) deleteSong(audioFile *AudioFile) (err error) {
|
|||||||
|
|
||||||
defaultTimedPopup(" Success ",
|
defaultTimedPopup(" Success ",
|
||||||
audioFile.name+"\nhas been deleted successfully")
|
audioFile.name+"\nhas been deleted successfully")
|
||||||
|
|
||||||
p.refresh()
|
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,8 +289,12 @@ func (p *Playlist) addAllToQueue(root *tview.TreeNode) {
|
|||||||
|
|
||||||
for _, v := range childrens {
|
for _, v := range childrens {
|
||||||
currNode := v.GetReference().(*AudioFile)
|
currNode := v.GetReference().(*AudioFile)
|
||||||
|
if currNode.isAudioFile {
|
||||||
|
if currNode != gomu.player.currentSong {
|
||||||
gomu.queue.enqueue(currNode)
|
gomu.queue.enqueue(currNode)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,7 +309,7 @@ func (p *Playlist) refresh() {
|
|||||||
|
|
||||||
populate(root, root.GetReference().(*AudioFile).path)
|
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
|
// to preserve previously highlighted node
|
||||||
if node.GetText() == prevFileName {
|
if node.GetText() == prevFileName {
|
||||||
@ -504,6 +528,11 @@ func (p *Playlist) rename(newName string) error {
|
|||||||
return tracerr.Wrap(err)
|
return tracerr.Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
audio.path = newPath
|
||||||
|
gomu.queue.saveQueue(false)
|
||||||
|
gomu.queue.clearQueue()
|
||||||
|
gomu.queue.loadQueue()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -592,18 +621,15 @@ func ytdl(url string, selPlaylist *tview.TreeNode) error {
|
|||||||
return tracerr.Wrap(err)
|
return tracerr.Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dir := viper.GetString("general.music_dir")
|
|
||||||
|
|
||||||
selAudioFile := selPlaylist.GetReference().(*AudioFile)
|
selAudioFile := selPlaylist.GetReference().(*AudioFile)
|
||||||
selPlaylistName := selAudioFile.name
|
dir := selAudioFile.path
|
||||||
|
|
||||||
defaultTimedPopup(" Ytdl ", "Downloading")
|
defaultTimedPopup(" Ytdl ", "Downloading")
|
||||||
|
|
||||||
// specify the output path for ytdl
|
// specify the output path for ytdl
|
||||||
outputDir := fmt.Sprintf(
|
outputDir := fmt.Sprintf(
|
||||||
"%s/%s/%%(title)s.%%(ext)s",
|
"%s/%%(title)s.%%(ext)s",
|
||||||
dir,
|
dir)
|
||||||
selPlaylistName)
|
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"--extract-audio",
|
"--extract-audio",
|
||||||
@ -611,6 +637,8 @@ func ytdl(url string, selPlaylist *tview.TreeNode) error {
|
|||||||
"mp3",
|
"mp3",
|
||||||
"--output",
|
"--output",
|
||||||
outputDir,
|
outputDir,
|
||||||
|
// "--cookies",
|
||||||
|
// "~/Downloads/youtube.com_cookies.txt",
|
||||||
url,
|
url,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -632,7 +660,7 @@ func ytdl(url string, selPlaylist *tview.TreeNode) error {
|
|||||||
return tracerr.Wrap(err)
|
return tracerr.Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
playlistPath := path.Join(expandTilde(dir), selPlaylistName)
|
playlistPath := dir
|
||||||
audioPath := extractFilePath(stdout.Bytes(), playlistPath)
|
audioPath := extractFilePath(stdout.Bytes(), playlistPath)
|
||||||
|
|
||||||
err = appendFile(expandTilde(viper.GetString("general.history_path")), url+"\n")
|
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)
|
return tracerr.Wrap(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFinishedMessage := fmt.Sprintf("Finished downloading\n%s",
|
downloadFinishedMessage := fmt.Sprintf("Finished downloading\n%s", getName(audioPath))
|
||||||
getName(audioPath))
|
|
||||||
|
|
||||||
defaultTimedPopup(" Ytdl ", downloadFinishedMessage)
|
defaultTimedPopup(" Ytdl ", downloadFinishedMessage)
|
||||||
|
gomu.app.Draw()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -664,19 +691,21 @@ func populate(root *tview.TreeNode, rootPath string) error {
|
|||||||
|
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
|
|
||||||
path := filepath.Join(rootPath, file.Name())
|
path, err := filepath.EvalSymlinks(filepath.Join(rootPath, file.Name()))
|
||||||
f, err := os.Open(path)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
songName := getName(file.Name())
|
songName := getName(file.Name())
|
||||||
child := tview.NewTreeNode(songName)
|
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)
|
filetype, err := getFileContentType(f)
|
||||||
|
|
||||||
@ -689,17 +718,10 @@ func populate(root *tview.TreeNode, rootPath string) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
audioLength, err := getLength(path)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
audioFile := &AudioFile{
|
audioFile := &AudioFile{
|
||||||
name: songName,
|
name: songName,
|
||||||
path: path,
|
path: path,
|
||||||
isAudioFile: true,
|
isAudioFile: true,
|
||||||
length: audioLength,
|
|
||||||
node: child,
|
node: child,
|
||||||
parent: root,
|
parent: root,
|
||||||
}
|
}
|
||||||
@ -716,11 +738,12 @@ func populate(root *tview.TreeNode, rootPath string) error {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if file.IsDir() {
|
if file.IsDir() || file.Mode()&os.ModeSymlink != 0 {
|
||||||
|
|
||||||
audioFile := &AudioFile{
|
audioFile := &AudioFile{
|
||||||
name: songName,
|
name: songName,
|
||||||
path: path,
|
path: path,
|
||||||
|
isAudioFile: false,
|
||||||
node: child,
|
node: child,
|
||||||
parent: root,
|
parent: root,
|
||||||
}
|
}
|
||||||
@ -743,3 +766,81 @@ func populate(root *tview.TreeNode, rootPath string) error {
|
|||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
47
popup.go
47
popup.go
@ -163,15 +163,15 @@ func timedPopup(
|
|||||||
|
|
||||||
box := tview.NewFrame(textView).SetBorders(1, 0, 0, 0, 0, 0)
|
box := tview.NewFrame(textView).SetBorders(1, 0, 0, 0, 0, 0)
|
||||||
box.SetTitle(title).SetBorder(true).SetBackgroundColor(gomu.colors.popup)
|
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++
|
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))
|
gomu.app.SetFocus(gomu.prevPanel.(tview.Primitive))
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(timeout)
|
time.Sleep(timeout)
|
||||||
gomu.pages.RemovePage(popupId)
|
gomu.pages.RemovePage(popupID)
|
||||||
gomu.app.Draw()
|
gomu.app.Draw()
|
||||||
|
|
||||||
// timed popup shouldn't get focused
|
// timed popup shouldn't get focused
|
||||||
@ -225,6 +225,10 @@ func helpPopup(panel Panel) {
|
|||||||
"q quit",
|
"q quit",
|
||||||
"+ volume up",
|
"+ volume up",
|
||||||
"- volume down",
|
"- volume down",
|
||||||
|
"f forward 10 seconds",
|
||||||
|
"F forward 60 seconds",
|
||||||
|
"b rewind 10 seconds",
|
||||||
|
"B rewind 60 seconds",
|
||||||
"? toggle help",
|
"? toggle help",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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+)?$`)
|
re := regexp.MustCompile(`^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$`)
|
||||||
|
|
||||||
popupId := "download-input-popup"
|
popupID := "download-input-popup"
|
||||||
input := newInputPopup(popupId, " Download ", "Url: ")
|
input := newInputPopup(popupID, " Download ", "Url: ", "")
|
||||||
|
|
||||||
input.SetDoneFunc(func(key tcell.Key) {
|
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
|
// Input popup that takes the name of directory to be created
|
||||||
func createPlaylistPopup() {
|
func createPlaylistPopup() {
|
||||||
|
|
||||||
popupId := "mkdir-input-popup"
|
popupID := "mkdir-input-popup"
|
||||||
input := newInputPopup(popupId, " New Playlist ", "Enter playlist name: ")
|
input := newInputPopup(popupID, " New Playlist ", "Enter playlist name: ", "")
|
||||||
|
|
||||||
input.SetDoneFunc(func(key tcell.Key) {
|
input.SetDoneFunc(func(key tcell.Key) {
|
||||||
|
|
||||||
@ -408,7 +412,6 @@ func searchPopup(stringsToMatch []string, handler func(selected string)) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
input.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey {
|
input.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey {
|
||||||
|
|
||||||
switch e.Key() {
|
switch e.Key() {
|
||||||
@ -462,7 +465,7 @@ func searchPopup(stringsToMatch []string, handler func(selected string)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Creates new popup widget with default settings
|
// 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().
|
inputField := tview.NewInputField().
|
||||||
SetLabel(label).
|
SetLabel(label).
|
||||||
@ -476,8 +479,10 @@ func newInputPopup(popupId, title, label string) *tview.InputField {
|
|||||||
SetBorder(true).
|
SetBorder(true).
|
||||||
SetBorderPadding(1, 0, 2, 2)
|
SetBorderPadding(1, 0, 2, 2)
|
||||||
|
|
||||||
|
inputField.SetText(text)
|
||||||
|
|
||||||
gomu.pages.
|
gomu.pages.
|
||||||
AddPage(popupId, center(inputField, 60, 5), true, true)
|
AddPage(popupID, center(inputField, 60, 5), true, true)
|
||||||
|
|
||||||
gomu.popups.push(inputField)
|
gomu.popups.push(inputField)
|
||||||
|
|
||||||
@ -486,8 +491,8 @@ func newInputPopup(popupId, title, label string) *tview.InputField {
|
|||||||
|
|
||||||
func renamePopup(node *AudioFile) {
|
func renamePopup(node *AudioFile) {
|
||||||
|
|
||||||
popupId := "rename-input-popup"
|
popupID := "rename-input-popup"
|
||||||
input := newInputPopup(popupId, " Rename ", "New name: ")
|
input := newInputPopup(popupID, " Rename ", "New name: ", node.name)
|
||||||
input.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey {
|
input.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey {
|
||||||
|
|
||||||
switch e.Key() {
|
switch e.Key() {
|
||||||
@ -501,12 +506,26 @@ func renamePopup(node *AudioFile) {
|
|||||||
defaultTimedPopup(" Error ", err.Error())
|
defaultTimedPopup(" Error ", err.Error())
|
||||||
logError(err)
|
logError(err)
|
||||||
}
|
}
|
||||||
gomu.pages.RemovePage(popupId)
|
gomu.pages.RemovePage(popupID)
|
||||||
gomu.popups.pop()
|
gomu.popups.pop()
|
||||||
gomu.playlist.refresh()
|
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:
|
case tcell.KeyEsc:
|
||||||
gomu.pages.RemovePage(popupId)
|
gomu.pages.RemovePage(popupID)
|
||||||
gomu.popups.pop()
|
gomu.popups.pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
57
queue.go
57
queue.go
@ -16,6 +16,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
"github.com/rivo/tview"
|
"github.com/rivo/tview"
|
||||||
|
"github.com/spf13/viper"
|
||||||
"github.com/ztrue/tracerr"
|
"github.com/ztrue/tracerr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -95,8 +96,25 @@ func (q *Queue) updateTitle() string {
|
|||||||
count = "song"
|
count = "song"
|
||||||
}
|
}
|
||||||
|
|
||||||
title := fmt.Sprintf("─ Queue ───┤ %d %s | %s ├",
|
var loop string
|
||||||
len(q.items), count, fmtTime)
|
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)
|
q.SetTitle(title)
|
||||||
|
|
||||||
@ -136,7 +154,9 @@ func (q *Queue) dequeue() (*AudioFile, error) {
|
|||||||
// Add item to the list and returns the length of the queue
|
// Add item to the list and returns the length of the queue
|
||||||
func (q *Queue) enqueue(audioFile *AudioFile) (int, error) {
|
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
|
||||||
|
|
||||||
@ -151,6 +171,11 @@ func (q *Queue) enqueue(audioFile *AudioFile) (int, error) {
|
|||||||
return q.GetItemCount(), nil
|
return q.GetItemCount(), nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !audioFile.isAudioFile {
|
||||||
|
return q.GetItemCount(), nil
|
||||||
|
}
|
||||||
|
|
||||||
q.items = append(q.items, audioFile)
|
q.items = append(q.items, audioFile)
|
||||||
songLength, err := getLength(audioFile.path)
|
songLength, err := getLength(audioFile.path)
|
||||||
@ -186,11 +211,25 @@ func (q *Queue) getItems() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save the current queue
|
// Save the current queue
|
||||||
func (q *Queue) saveQueue() error {
|
func (q *Queue) saveQueue(isQuit bool) error {
|
||||||
|
|
||||||
songPaths := q.getItems()
|
songPaths := q.getItems()
|
||||||
var content strings.Builder
|
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 {
|
for _, songPath := range songPaths {
|
||||||
// hashed song name is easier to search through
|
// hashed song name is easier to search through
|
||||||
hashed := sha1Hex(getName(songPath))
|
hashed := sha1Hex(getName(songPath))
|
||||||
@ -360,7 +399,7 @@ func (q *Queue) shuffle() {
|
|||||||
q.AddItem(queueText, v.path, 0, nil)
|
q.AddItem(queueText, v.path, 0, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
q.updateTitle()
|
// q.updateTitle()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -421,3 +460,11 @@ func sha1Hex(input string) string {
|
|||||||
h.Write([]byte(input))
|
h.Write([]byte(input))
|
||||||
return hex.EncodeToString(h.Sum(nil))
|
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
|
||||||
|
}
|
||||||
|
43
start.go
43
start.go
@ -18,7 +18,7 @@ import (
|
|||||||
"github.com/spf13/viper"
|
"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 {
|
type Panel interface {
|
||||||
HasFocus() bool
|
HasFocus() bool
|
||||||
SetBorderColor(color tcell.Color) *tview.Box
|
SetBorderColor(color tcell.Color) *tview.Box
|
||||||
@ -29,9 +29,9 @@ type Panel interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CONFIG_PATH = ".config/gomu/config"
|
configPath = ".config/gomu/config"
|
||||||
HISTORY_PATH = "~/.local/share/gomu/urls"
|
historyPath = "~/.local/share/gomu/urls"
|
||||||
MUSIC_PATH = "~/music"
|
musicPath = "~/music"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Reads config file and sets the options
|
// Reads config file and sets the options
|
||||||
@ -43,6 +43,7 @@ general:
|
|||||||
confirm_bulk_add: true
|
confirm_bulk_add: true
|
||||||
confirm_on_exit: true
|
confirm_on_exit: true
|
||||||
load_prev_queue: true
|
load_prev_queue: true
|
||||||
|
queue_loop: true
|
||||||
# change this to directory that contains mp3 files
|
# change this to directory that contains mp3 files
|
||||||
music_dir: ~/music
|
music_dir: ~/music
|
||||||
# url history of downloaded audio will be saved here
|
# 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
|
# if you experiencing error using this invidious instance, you can change it
|
||||||
# to another instance from this list:
|
# to another instance from this list:
|
||||||
# https://github.com/iv-org/documentation/blob/master/Invidious-Instances.md
|
# 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
|
# not all colors can be reproducible in terminal
|
||||||
# changing hex colors may or may not produce expected result
|
# changing hex colors may or may not produce expected result
|
||||||
@ -79,6 +80,8 @@ color:
|
|||||||
emoji:
|
emoji:
|
||||||
playlist:
|
playlist:
|
||||||
file:
|
file:
|
||||||
|
loop: ﯩ
|
||||||
|
noloop:
|
||||||
|
|
||||||
# vi:ft=yaml
|
# vi:ft=yaml
|
||||||
`
|
`
|
||||||
@ -91,7 +94,7 @@ emoji:
|
|||||||
logError(err)
|
logError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultPath := path.Join(home, CONFIG_PATH)
|
defaultPath := path.Join(home, configPath)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logError(err)
|
logError(err)
|
||||||
@ -103,19 +106,18 @@ emoji:
|
|||||||
viper.AddConfigPath("$HOME/.config/gomu")
|
viper.AddConfigPath("$HOME/.config/gomu")
|
||||||
|
|
||||||
// General config
|
// General config
|
||||||
viper.SetDefault("general.music_dir", MUSIC_PATH)
|
viper.SetDefault("general.music_dir", musicPath)
|
||||||
viper.SetDefault("general.history_path", HISTORY_PATH)
|
viper.SetDefault("general.history_path", historyPath)
|
||||||
viper.SetDefault("general.confirm_on_exit", true)
|
viper.SetDefault("general.confirm_on_exit", true)
|
||||||
viper.SetDefault("general.confirm_bulk_add", true)
|
viper.SetDefault("general.confirm_bulk_add", true)
|
||||||
viper.SetDefault("general.popup_timeout", "5s")
|
viper.SetDefault("general.popup_timeout", "5s")
|
||||||
viper.SetDefault("general.volume", 100)
|
viper.SetDefault("general.volume", 100)
|
||||||
viper.SetDefault("general.load_prev_queue", true)
|
viper.SetDefault("general.load_prev_queue", true)
|
||||||
viper.SetDefault("general.use_emoji", false)
|
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 {
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
|
||||||
|
|
||||||
// creates gomu config dir if does not exist
|
// creates gomu config dir if does not exist
|
||||||
if _, err := os.Stat(defaultPath); err != nil {
|
if _, err := os.Stat(defaultPath); err != nil {
|
||||||
if err := os.MkdirAll(home+"/.config/gomu", 0755); err != nil {
|
if err := os.MkdirAll(home+"/.config/gomu", 0755); err != nil {
|
||||||
@ -146,9 +148,9 @@ type Args struct {
|
|||||||
|
|
||||||
func getArgs() Args {
|
func getArgs() Args {
|
||||||
ar := 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"),
|
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"),
|
version: flag.Bool("version", false, "Print gomu version"),
|
||||||
}
|
}
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
@ -199,6 +201,9 @@ func start(application *tview.Application, args Args) {
|
|||||||
gomu.setFocusPanel(gomu.playlist)
|
gomu.setFocusPanel(gomu.playlist)
|
||||||
gomu.prevPanel = 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") {
|
if !*args.empty && viper.GetBool("general.load_prev_queue") {
|
||||||
// load saved queue from previous session
|
// load saved queue from previous session
|
||||||
if err := gomu.queue.loadQueue(); err != nil {
|
if err := gomu.queue.loadQueue(); err != nil {
|
||||||
@ -207,14 +212,14 @@ func start(application *tview.Application, args Args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sigs := make(chan os.Signal, 1)
|
sigs := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGKILL)
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||||
go func() {
|
go func() {
|
||||||
sig := <-sigs
|
sig := <-sigs
|
||||||
errMsg := fmt.Sprintf("Received %s. Exiting program", sig.String())
|
errMsg := fmt.Sprintf("Received %s. Exiting program", sig.String())
|
||||||
logError(errors.New(errMsg))
|
logError(errors.New(errMsg))
|
||||||
err := gomu.quit(args)
|
err := gomu.quit(args)
|
||||||
if err != nil {
|
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-") {
|
if strings.Contains(popupName, "confirmation-") {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
gomu.cyclePanels()
|
gomu.cyclePanels2()
|
||||||
}
|
}
|
||||||
|
|
||||||
cmds := map[rune]string{
|
cmds := map[rune]string{
|
||||||
'q': "quit",
|
'q': "quit",
|
||||||
' ': "toggle_pause",
|
' ': "toggle_pause",
|
||||||
'+': "volume_up",
|
'+': "volume_up",
|
||||||
|
'=': "volume_up",
|
||||||
'-': "volume_down",
|
'-': "volume_down",
|
||||||
|
'_': "volume_down",
|
||||||
'n': "skip",
|
'n': "skip",
|
||||||
':': "command_search",
|
':': "command_search",
|
||||||
'?': "toggle_help",
|
'?': "toggle_help",
|
||||||
|
'f': "forward",
|
||||||
|
'F': "forward_fast",
|
||||||
|
'b': "rewind",
|
||||||
|
'B': "rewind_fast",
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, cmd := range cmds {
|
for key, cmd := range cmds {
|
||||||
@ -268,8 +279,10 @@ func start(application *tview.Application, args Args) {
|
|||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
go populateAudioLength(gomu.playlist.GetRoot())
|
||||||
// main loop
|
// main loop
|
||||||
if err := application.SetRoot(gomu.pages, true).SetFocus(gomu.playlist).Run(); err != nil {
|
if err := application.SetRoot(gomu.pages, true).SetFocus(gomu.playlist).Run(); err != nil {
|
||||||
logError(err)
|
logError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
1
utils.go
1
utils.go
@ -21,7 +21,6 @@ func logError(err error) {
|
|||||||
log.Println(tracerr.Sprint(err))
|
log.Println(tracerr.Sprint(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Formats duration to my desired output mm:ss
|
// Formats duration to my desired output mm:ss
|
||||||
func fmtDuration(input time.Duration) string {
|
func fmtDuration(input time.Duration) string {
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user