2020-06-22 00:05:56 +08:00
|
|
|
// Copyright (C) 2020 Raziman
|
|
|
|
|
2020-06-19 16:22:20 +08:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2020-07-03 11:17:45 +08:00
|
|
|
"bytes"
|
2020-06-23 22:27:02 +08:00
|
|
|
"fmt"
|
2020-06-19 16:22:20 +08:00
|
|
|
"io/ioutil"
|
2020-08-11 13:39:02 +08:00
|
|
|
"log"
|
2020-06-23 18:42:27 +08:00
|
|
|
"os"
|
2020-07-03 11:17:45 +08:00
|
|
|
"os/exec"
|
2020-06-25 10:46:45 +08:00
|
|
|
"path"
|
2020-06-19 16:22:20 +08:00
|
|
|
"path/filepath"
|
2020-07-22 21:01:13 +08:00
|
|
|
"strings"
|
2020-06-28 13:41:14 +08:00
|
|
|
"time"
|
2020-06-19 16:22:20 +08:00
|
|
|
|
2021-01-21 01:30:16 +08:00
|
|
|
"github.com/gdamore/tcell/v2"
|
2020-06-19 16:22:20 +08:00
|
|
|
"github.com/rivo/tview"
|
2020-06-24 20:09:47 +08:00
|
|
|
"github.com/spf13/viper"
|
2020-07-24 22:27:08 +08:00
|
|
|
spin "github.com/tj/go-spin"
|
2020-07-21 12:22:00 +08:00
|
|
|
"github.com/ztrue/tracerr"
|
2020-06-19 16:22:20 +08:00
|
|
|
)
|
|
|
|
|
2020-07-23 15:34:56 +08:00
|
|
|
// Playlist and mp3 files are represented with this struct
|
|
|
|
// if isAudioFile equals to false it is a directory
|
2020-06-19 16:22:20 +08:00
|
|
|
type AudioFile struct {
|
2020-07-23 15:15:39 +08:00
|
|
|
name string
|
|
|
|
path string
|
|
|
|
isAudioFile bool
|
|
|
|
length time.Duration
|
|
|
|
node *tview.TreeNode
|
|
|
|
parent *tview.TreeNode
|
2020-06-19 16:22:20 +08:00
|
|
|
}
|
|
|
|
|
2020-07-25 22:53:54 +08:00
|
|
|
// Playlist struct represents playlist panel
|
|
|
|
// that shows the tree of the music directory
|
2020-06-26 12:54:48 +08:00
|
|
|
type Playlist struct {
|
|
|
|
*tview.TreeView
|
2020-07-24 22:27:08 +08:00
|
|
|
prevNode *tview.TreeNode
|
|
|
|
defaultTitle string
|
|
|
|
// number of downloads
|
|
|
|
download int
|
|
|
|
done chan struct{}
|
2020-06-26 12:54:48 +08:00
|
|
|
}
|
2020-06-19 16:22:20 +08:00
|
|
|
|
2020-07-23 15:15:39 +08:00
|
|
|
func (p *Playlist) help() []string {
|
2020-07-17 15:34:50 +08:00
|
|
|
|
|
|
|
return []string{
|
|
|
|
"j down",
|
|
|
|
"k up",
|
|
|
|
"h close node",
|
2020-08-15 11:25:57 +08:00
|
|
|
"a create a playlist",
|
2020-07-17 15:34:50 +08:00
|
|
|
"l add song to queue",
|
|
|
|
"L add playlist to queue",
|
|
|
|
"d delete file from filesystem",
|
|
|
|
"D delete playlist from filesystem",
|
|
|
|
"Y download audio",
|
|
|
|
"r refresh",
|
2020-08-18 16:09:43 +08:00
|
|
|
"R rename",
|
2020-07-24 23:06:34 +08:00
|
|
|
"/ find in playlist",
|
2020-07-17 15:34:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-07-24 22:27:08 +08:00
|
|
|
// newPlaylist returns new instance of playlist and runs populate function
|
|
|
|
// on root music directory.
|
2020-08-11 13:39:02 +08:00
|
|
|
func newPlaylist(args Args) *Playlist {
|
2020-06-24 20:09:47 +08:00
|
|
|
|
2020-07-28 11:24:58 +08:00
|
|
|
rootDir, err := filepath.Abs(expandTilde(viper.GetString("general.music_dir")))
|
2020-06-19 16:22:20 +08:00
|
|
|
|
2020-08-11 13:39:02 +08:00
|
|
|
// if not default value was given
|
|
|
|
if *args.music != "~/music" {
|
|
|
|
rootDir = expandFilePath(*args.music)
|
|
|
|
}
|
|
|
|
|
2020-06-19 16:22:20 +08:00
|
|
|
if err != nil {
|
2020-08-11 13:39:02 +08:00
|
|
|
log.Fatalf("Unable to find music directory: %e", err)
|
2020-06-19 16:22:20 +08:00
|
|
|
}
|
|
|
|
|
2020-08-25 22:05:17 +08:00
|
|
|
rootTextView := fmt.Sprintf("%s %s",
|
|
|
|
viper.GetString("emoji.playlist"), path.Base(rootDir))
|
|
|
|
|
|
|
|
root := tview.NewTreeNode(rootTextView).
|
2020-08-22 14:41:03 +08:00
|
|
|
SetColor(gomu.colors.accent)
|
2020-06-19 16:22:20 +08:00
|
|
|
|
|
|
|
tree := tview.NewTreeView().SetRoot(root)
|
2020-06-28 13:41:14 +08:00
|
|
|
|
2020-07-17 15:34:50 +08:00
|
|
|
playlist := &Playlist{
|
2020-07-24 22:27:08 +08:00
|
|
|
TreeView: tree,
|
|
|
|
defaultTitle: "─ Playlist ──┤ 0 downloads ├",
|
|
|
|
done: make(chan struct{}),
|
2020-07-17 15:34:50 +08:00
|
|
|
}
|
2020-06-28 13:41:14 +08:00
|
|
|
|
|
|
|
rootAudioFile := &AudioFile{
|
2020-08-25 22:05:17 +08:00
|
|
|
name: path.Base(rootDir),
|
2020-07-23 15:15:39 +08:00
|
|
|
node: root,
|
|
|
|
path: rootDir,
|
2020-06-28 13:41:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
root.SetReference(rootAudioFile)
|
2020-06-19 16:22:20 +08:00
|
|
|
|
2020-07-10 22:05:36 +08:00
|
|
|
playlist.
|
2020-07-24 22:27:08 +08:00
|
|
|
SetTitle(playlist.defaultTitle).
|
2020-07-10 22:05:36 +08:00
|
|
|
SetBorder(true).
|
2020-08-25 21:12:14 +08:00
|
|
|
SetTitleAlign(tview.AlignLeft).
|
|
|
|
SetBorderPadding(0, 0, 1, 1)
|
2020-06-19 16:22:20 +08:00
|
|
|
|
2020-06-25 14:12:19 +08:00
|
|
|
populate(root, rootDir)
|
2020-06-19 16:22:20 +08:00
|
|
|
|
2020-06-25 14:12:19 +08:00
|
|
|
var firstChild *tview.TreeNode
|
2020-06-23 18:42:27 +08:00
|
|
|
|
2020-06-25 14:12:19 +08:00
|
|
|
if len(root.GetChildren()) == 0 {
|
|
|
|
firstChild = root
|
|
|
|
} else {
|
|
|
|
firstChild = root.GetChildren()[0]
|
|
|
|
}
|
2020-06-19 16:22:20 +08:00
|
|
|
|
2020-07-23 15:15:39 +08:00
|
|
|
playlist.setHighlight(firstChild)
|
2020-06-19 16:22:20 +08:00
|
|
|
|
2020-06-26 12:54:48 +08:00
|
|
|
playlist.SetChangedFunc(func(node *tview.TreeNode) {
|
2020-07-23 15:15:39 +08:00
|
|
|
playlist.setHighlight(node)
|
2020-06-25 14:12:19 +08:00
|
|
|
})
|
2020-06-23 18:42:27 +08:00
|
|
|
|
2020-06-26 12:54:48 +08:00
|
|
|
playlist.SetSelectedFunc(func(node *tview.TreeNode) {
|
|
|
|
node.SetExpanded(!node.IsExpanded())
|
|
|
|
})
|
2020-06-19 16:22:20 +08:00
|
|
|
|
2020-06-26 12:54:48 +08:00
|
|
|
playlist.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey {
|
|
|
|
|
2020-08-25 18:17:54 +08:00
|
|
|
cmds := map[rune]string{
|
|
|
|
'a': "create_playlist",
|
|
|
|
'D': "delete_playlist",
|
|
|
|
'd': "delete_file",
|
|
|
|
'Y': "download_audio",
|
|
|
|
'l': "add_queue",
|
|
|
|
'L': "bulk_add",
|
|
|
|
'h': "close_node",
|
|
|
|
'r': "refresh",
|
|
|
|
'R': "rename",
|
|
|
|
'/': "playlist_search",
|
|
|
|
}
|
2020-07-01 17:47:52 +08:00
|
|
|
|
2020-08-25 18:17:54 +08:00
|
|
|
for key, cmd := range cmds {
|
|
|
|
if e.Rune() != key {
|
|
|
|
continue
|
2020-07-01 17:47:52 +08:00
|
|
|
}
|
2020-08-25 18:17:54 +08:00
|
|
|
fn, err := gomu.command.getFn(cmd)
|
2020-07-20 21:48:13 +08:00
|
|
|
if err != nil {
|
2020-07-23 15:15:39 +08:00
|
|
|
logError(err)
|
2020-06-28 13:41:14 +08:00
|
|
|
return e
|
|
|
|
}
|
2020-08-25 18:17:54 +08:00
|
|
|
fn()
|
|
|
|
}
|
2020-06-28 13:41:14 +08:00
|
|
|
|
2020-08-25 18:17:54 +08:00
|
|
|
// disable default key handler for space
|
|
|
|
if e.Rune() == ' ' {
|
|
|
|
return nil
|
2020-06-26 12:54:48 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
return e
|
2020-06-19 16:22:20 +08:00
|
|
|
})
|
|
|
|
|
2020-06-26 12:54:48 +08:00
|
|
|
return playlist
|
2020-06-19 16:22:20 +08:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-08-25 18:17:54 +08:00
|
|
|
// Returns the current file highlighted in the playlist
|
|
|
|
func (p Playlist) getCurrentFile() *AudioFile {
|
|
|
|
node := p.GetCurrentNode()
|
|
|
|
if node == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return node.GetReference().(*AudioFile)
|
|
|
|
}
|
|
|
|
|
2020-07-23 15:34:56 +08:00
|
|
|
// Deletes song from filesystem
|
2020-07-23 15:15:39 +08:00
|
|
|
func (p *Playlist) deleteSong(audioFile *AudioFile) (err error) {
|
2020-07-20 21:48:13 +08:00
|
|
|
|
|
|
|
confirmationPopup(
|
|
|
|
"Are you sure to delete this audio file?", func(_ int, buttonName string) {
|
|
|
|
|
2020-07-22 21:01:13 +08:00
|
|
|
if buttonName == "no" || buttonName == "" {
|
2020-07-20 21:48:13 +08:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-07-23 15:15:39 +08:00
|
|
|
err := os.Remove(audioFile.path)
|
2020-07-20 21:48:13 +08:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
2020-08-25 20:45:16 +08:00
|
|
|
defaultTimedPopup(" Error ", "Unable to delete "+audioFile.name)
|
2020-07-20 21:48:13 +08:00
|
|
|
|
2020-07-21 12:22:00 +08:00
|
|
|
err = tracerr.Wrap(err)
|
2020-07-20 21:48:13 +08:00
|
|
|
|
|
|
|
} else {
|
|
|
|
|
2020-08-25 20:45:16 +08:00
|
|
|
defaultTimedPopup(" Success ",
|
|
|
|
audioFile.name+"\nhas been deleted successfully")
|
2020-07-20 21:48:13 +08:00
|
|
|
|
2020-07-23 15:15:39 +08:00
|
|
|
p.refresh()
|
2020-07-20 21:48:13 +08:00
|
|
|
}
|
|
|
|
|
2020-07-21 01:21:59 +08:00
|
|
|
})
|
2020-07-20 21:48:13 +08:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Deletes playlist/dir from filesystem
|
2020-07-23 15:15:39 +08:00
|
|
|
func (p *Playlist) deletePlaylist(audioFile *AudioFile) (err error) {
|
2020-07-20 21:48:13 +08:00
|
|
|
|
|
|
|
var selectedDir *AudioFile
|
|
|
|
|
|
|
|
// gets the parent dir if current focused node is not a dir
|
2020-07-23 15:15:39 +08:00
|
|
|
if audioFile.isAudioFile {
|
|
|
|
selectedDir = audioFile.parent.GetReference().(*AudioFile)
|
2020-07-20 21:48:13 +08:00
|
|
|
} else {
|
|
|
|
selectedDir = audioFile
|
|
|
|
}
|
|
|
|
|
2020-07-21 01:21:59 +08:00
|
|
|
confirmationPopup("Are you sure to delete this directory?",
|
2020-07-20 21:48:13 +08:00
|
|
|
func(_ int, buttonName string) {
|
|
|
|
|
2020-07-22 21:01:13 +08:00
|
|
|
if buttonName == "no" || buttonName == "" {
|
2020-07-20 21:48:13 +08:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-07-23 15:15:39 +08:00
|
|
|
err := os.RemoveAll(selectedDir.path)
|
2020-07-20 21:48:13 +08:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
2020-08-25 20:45:16 +08:00
|
|
|
defaultTimedPopup(
|
2020-07-20 21:48:13 +08:00
|
|
|
" Error ",
|
2020-08-25 20:45:16 +08:00
|
|
|
"Unable to delete dir "+selectedDir.name)
|
2020-07-20 21:48:13 +08:00
|
|
|
|
2020-07-21 12:22:00 +08:00
|
|
|
err = tracerr.Wrap(err)
|
2020-07-20 21:48:13 +08:00
|
|
|
|
|
|
|
} else {
|
|
|
|
|
2020-08-25 20:45:16 +08:00
|
|
|
defaultTimedPopup(
|
2020-07-20 21:48:13 +08:00
|
|
|
" Success ",
|
2020-08-25 20:45:16 +08:00
|
|
|
selectedDir.name+"\nhas been deleted successfully")
|
2020-07-20 21:48:13 +08:00
|
|
|
|
2020-07-23 15:15:39 +08:00
|
|
|
p.refresh()
|
2020-07-20 21:48:13 +08:00
|
|
|
}
|
|
|
|
|
2020-07-21 01:21:59 +08:00
|
|
|
})
|
2020-07-20 21:48:13 +08:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-07-04 16:16:57 +08:00
|
|
|
// Bulk add a playlist to queue
|
2020-07-23 15:15:39 +08:00
|
|
|
func (p *Playlist) addAllToQueue(root *tview.TreeNode) {
|
2020-06-25 14:12:19 +08:00
|
|
|
|
|
|
|
var childrens []*tview.TreeNode
|
|
|
|
childrens = root.GetChildren()
|
|
|
|
|
2020-07-01 21:21:09 +08:00
|
|
|
// gets the parent if the highlighted item is a file
|
2020-07-23 15:15:39 +08:00
|
|
|
if root.GetReference().(*AudioFile).isAudioFile {
|
|
|
|
childrens = root.GetReference().(*AudioFile).parent.GetChildren()
|
2020-06-26 12:54:48 +08:00
|
|
|
}
|
2020-06-25 14:12:19 +08:00
|
|
|
|
|
|
|
for _, v := range childrens {
|
|
|
|
currNode := v.GetReference().(*AudioFile)
|
2020-07-23 15:15:39 +08:00
|
|
|
gomu.queue.enqueue(currNode)
|
2020-06-25 14:12:19 +08:00
|
|
|
}
|
2020-06-25 10:46:45 +08:00
|
|
|
|
|
|
|
}
|
2020-06-28 13:41:14 +08:00
|
|
|
|
2020-07-23 15:34:56 +08:00
|
|
|
// Refreshes the playlist and read the whole root music dir
|
2020-07-23 15:15:39 +08:00
|
|
|
func (p *Playlist) refresh() {
|
2020-06-28 13:41:14 +08:00
|
|
|
|
2020-07-23 15:15:39 +08:00
|
|
|
root := gomu.playlist.GetRoot()
|
2020-06-28 13:41:14 +08:00
|
|
|
|
2020-07-23 15:15:39 +08:00
|
|
|
prevFileName := gomu.playlist.GetCurrentNode().GetText()
|
2020-06-28 13:41:14 +08:00
|
|
|
|
|
|
|
root.ClearChildren()
|
|
|
|
|
2020-07-23 15:15:39 +08:00
|
|
|
populate(root, root.GetReference().(*AudioFile).path)
|
2020-06-28 13:41:14 +08:00
|
|
|
|
|
|
|
root.Walk(func(node, parent *tview.TreeNode) bool {
|
|
|
|
|
|
|
|
// to preserve previously highlighted node
|
2020-07-13 14:33:29 +08:00
|
|
|
if node.GetText() == prevFileName {
|
2020-07-23 15:15:39 +08:00
|
|
|
p.setHighlight(node)
|
2020-06-28 13:41:14 +08:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
})
|
2020-07-01 17:47:52 +08:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-07-04 16:16:57 +08:00
|
|
|
// Adds child while setting reference to audio file
|
2020-07-23 15:15:39 +08:00
|
|
|
func (p *Playlist) addSongToPlaylist(
|
2020-07-20 21:48:13 +08:00
|
|
|
audioPath string, selPlaylist *tview.TreeNode,
|
|
|
|
) error {
|
2020-07-01 17:47:52 +08:00
|
|
|
|
|
|
|
f, err := os.Open(audioPath)
|
|
|
|
|
|
|
|
if err != nil {
|
2020-07-21 12:22:00 +08:00
|
|
|
return tracerr.Wrap(err)
|
2020-07-01 17:47:52 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
defer f.Close()
|
|
|
|
|
2020-08-11 18:31:33 +08:00
|
|
|
songName := getName(audioPath)
|
|
|
|
node := tview.NewTreeNode(songName)
|
2020-07-01 17:47:52 +08:00
|
|
|
|
2020-07-23 15:15:39 +08:00
|
|
|
audioLength, err := getLength(audioPath)
|
2020-07-01 17:47:52 +08:00
|
|
|
|
|
|
|
if err != nil {
|
2020-07-21 12:22:00 +08:00
|
|
|
return tracerr.Wrap(err)
|
2020-07-01 17:47:52 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
audioFile := &AudioFile{
|
2020-08-14 22:27:28 +08:00
|
|
|
name: songName,
|
2020-07-23 15:15:39 +08:00
|
|
|
path: audioPath,
|
|
|
|
isAudioFile: true,
|
|
|
|
length: audioLength,
|
2020-08-14 22:27:28 +08:00
|
|
|
node: node,
|
2020-07-23 15:15:39 +08:00
|
|
|
parent: selPlaylist,
|
2020-07-01 17:47:52 +08:00
|
|
|
}
|
|
|
|
|
2020-08-11 18:31:33 +08:00
|
|
|
displayText := songName
|
|
|
|
|
|
|
|
if viper.GetBool("general.emoji") {
|
|
|
|
displayText = fmt.Sprintf("🎵 %s", songName)
|
|
|
|
}
|
|
|
|
|
2020-07-01 17:47:52 +08:00
|
|
|
node.SetReference(audioFile)
|
2020-08-11 18:31:33 +08:00
|
|
|
node.SetText(displayText)
|
2020-08-15 11:25:57 +08:00
|
|
|
|
2020-07-01 17:47:52 +08:00
|
|
|
selPlaylist.AddChild(node)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-07-22 21:00:14 +08:00
|
|
|
// Gets all audio files walks from music root directory
|
2020-07-23 15:15:39 +08:00
|
|
|
func (p *Playlist) getAudioFiles() []*AudioFile {
|
2020-07-10 22:05:36 +08:00
|
|
|
|
2020-07-10 16:52:37 +08:00
|
|
|
root := p.GetRoot()
|
|
|
|
|
|
|
|
audioFiles := []*AudioFile{}
|
|
|
|
|
|
|
|
root.Walk(func(node, _ *tview.TreeNode) bool {
|
|
|
|
|
|
|
|
audioFile := node.GetReference().(*AudioFile)
|
|
|
|
audioFiles = append(audioFiles, audioFile)
|
2020-07-10 22:05:36 +08:00
|
|
|
|
2020-07-10 16:52:37 +08:00
|
|
|
return true
|
|
|
|
})
|
|
|
|
|
|
|
|
return audioFiles
|
|
|
|
}
|
|
|
|
|
2020-07-04 16:16:57 +08:00
|
|
|
// Creates a directory under selected node, returns error if playlist exists
|
2020-07-23 15:15:39 +08:00
|
|
|
func (p *Playlist) createPlaylist(name string) error {
|
2020-07-01 17:47:52 +08:00
|
|
|
|
|
|
|
selectedNode := p.GetCurrentNode()
|
|
|
|
|
2020-07-23 15:15:39 +08:00
|
|
|
parentNode := selectedNode.GetReference().(*AudioFile).parent
|
2020-07-01 17:47:52 +08:00
|
|
|
|
|
|
|
// if the current node is the root
|
|
|
|
// sets the parent to itself
|
|
|
|
if parentNode == nil {
|
|
|
|
parentNode = selectedNode
|
|
|
|
}
|
|
|
|
|
|
|
|
audioFile := parentNode.GetReference().(*AudioFile)
|
|
|
|
|
2020-07-23 15:15:39 +08:00
|
|
|
err := os.Mkdir(path.Join(audioFile.path, name), 0744)
|
2020-07-01 17:47:52 +08:00
|
|
|
|
|
|
|
if err != nil {
|
2020-07-21 12:22:00 +08:00
|
|
|
return tracerr.Wrap(err)
|
2020-07-01 17:47:52 +08:00
|
|
|
}
|
|
|
|
|
2020-07-23 15:15:39 +08:00
|
|
|
p.refresh()
|
2020-07-01 17:47:52 +08:00
|
|
|
|
|
|
|
return nil
|
|
|
|
|
2020-06-28 13:41:14 +08:00
|
|
|
}
|
2020-07-01 21:21:09 +08:00
|
|
|
|
2020-07-04 16:16:57 +08:00
|
|
|
// This is used to replace default behaviour of SetCurrentNode which
|
2020-07-01 21:21:09 +08:00
|
|
|
// adds color highlight attributes
|
2020-07-23 15:15:39 +08:00
|
|
|
func (p *Playlist) setHighlight(currNode *tview.TreeNode) {
|
2020-07-01 21:21:09 +08:00
|
|
|
|
|
|
|
if p.prevNode != nil {
|
2020-08-22 14:41:03 +08:00
|
|
|
p.prevNode.SetColor(gomu.colors.foreground)
|
2020-07-01 21:21:09 +08:00
|
|
|
}
|
2020-08-22 14:41:03 +08:00
|
|
|
currNode.SetColor(gomu.colors.accent)
|
2020-07-01 21:21:09 +08:00
|
|
|
p.SetCurrentNode(currNode)
|
|
|
|
|
2020-07-23 15:15:39 +08:00
|
|
|
if currNode.GetReference().(*AudioFile).isAudioFile {
|
2020-07-01 21:21:09 +08:00
|
|
|
p.prevNode = currNode
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2020-07-03 11:17:45 +08:00
|
|
|
|
2020-07-06 18:58:27 +08:00
|
|
|
// Traverses the playlist and finds the AudioFile struct
|
2020-07-10 16:52:37 +08:00
|
|
|
// audioName must be hashed with sha1 first
|
2020-07-23 15:15:39 +08:00
|
|
|
func (p *Playlist) findAudioFile(audioName string) (*AudioFile, error) {
|
2020-07-06 18:58:27 +08:00
|
|
|
|
|
|
|
root := p.GetRoot()
|
|
|
|
|
|
|
|
var selNode *AudioFile
|
|
|
|
|
|
|
|
root.Walk(func(node, _ *tview.TreeNode) bool {
|
|
|
|
|
|
|
|
audioFile := node.GetReference().(*AudioFile)
|
|
|
|
|
2020-07-23 15:15:39 +08:00
|
|
|
hashed := sha1Hex(getName(audioFile.name))
|
2020-07-10 16:52:37 +08:00
|
|
|
|
|
|
|
if hashed == audioName {
|
2020-07-06 18:58:27 +08:00
|
|
|
selNode = audioFile
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
})
|
|
|
|
|
2020-07-21 12:22:00 +08:00
|
|
|
if selNode == nil {
|
|
|
|
return nil, tracerr.New("no matching audio name")
|
|
|
|
}
|
2020-07-06 18:58:27 +08:00
|
|
|
|
2020-07-21 12:22:00 +08:00
|
|
|
return selNode, nil
|
2020-07-06 18:58:27 +08:00
|
|
|
}
|
2020-07-03 11:17:45 +08:00
|
|
|
|
2020-07-22 21:00:14 +08:00
|
|
|
// Highlight the selected node searched using fzf
|
2020-07-23 15:15:39 +08:00
|
|
|
func (p *Playlist) fuzzyFind() error {
|
2020-07-22 21:00:14 +08:00
|
|
|
|
|
|
|
var result string
|
|
|
|
var err error
|
|
|
|
|
2020-07-23 15:15:39 +08:00
|
|
|
audioFiles := p.getAudioFiles()
|
2020-07-22 21:00:14 +08:00
|
|
|
paths := make(map[string]*tview.TreeNode, len(audioFiles))
|
|
|
|
input := make([]string, 0, len(audioFiles))
|
|
|
|
|
|
|
|
for _, v := range audioFiles {
|
2020-07-23 15:15:39 +08:00
|
|
|
rootDir := audioFiles[0].path + "/"
|
2020-07-22 21:00:14 +08:00
|
|
|
// path relative to music directory
|
2020-07-23 15:15:39 +08:00
|
|
|
shortPath := strings.TrimPrefix(v.path, rootDir)
|
|
|
|
paths[shortPath] = v.node
|
2020-07-22 21:00:14 +08:00
|
|
|
input = append(input, shortPath)
|
|
|
|
}
|
|
|
|
|
2020-07-23 15:15:39 +08:00
|
|
|
gomu.suspend()
|
|
|
|
ok := gomu.app.Suspend(func() {
|
|
|
|
res, e := fzfFind(input)
|
2020-07-22 21:00:14 +08:00
|
|
|
if e != nil {
|
|
|
|
err = tracerr.Wrap(e)
|
|
|
|
}
|
|
|
|
result = res
|
|
|
|
})
|
2020-07-23 15:15:39 +08:00
|
|
|
gomu.unsuspend()
|
2020-07-22 21:00:14 +08:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return tracerr.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !ok {
|
|
|
|
return tracerr.New("App was not suspended")
|
|
|
|
}
|
|
|
|
|
|
|
|
if result == "" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return tracerr.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var selNode *tview.TreeNode
|
2020-07-25 22:54:53 +08:00
|
|
|
selNode, ok = paths[result]
|
|
|
|
|
|
|
|
if ok {
|
|
|
|
p.setHighlight(selNode)
|
|
|
|
}
|
2020-07-22 21:00:14 +08:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-08-18 16:09:43 +08:00
|
|
|
func (p *Playlist) rename(newName string) error {
|
|
|
|
|
|
|
|
currentNode := p.GetCurrentNode()
|
|
|
|
audio := currentNode.GetReference().(*AudioFile)
|
|
|
|
pathToFile, _ := filepath.Split(audio.path)
|
|
|
|
var newPath string
|
|
|
|
if audio.isAudioFile {
|
|
|
|
newPath = pathToFile + newName + ".mp3"
|
|
|
|
} else {
|
|
|
|
newPath = pathToFile + newName
|
|
|
|
}
|
|
|
|
err := os.Rename(audio.path, newPath)
|
|
|
|
if err != nil {
|
|
|
|
return tracerr.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-07-25 22:53:54 +08:00
|
|
|
// updateTitle creates a spinning motion on the title
|
|
|
|
// of the playlist panel when downloading.
|
2020-07-24 22:27:08 +08:00
|
|
|
func (p *Playlist) updateTitle() {
|
|
|
|
|
|
|
|
if p.download == 0 {
|
|
|
|
p.SetTitle(p.defaultTitle)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-07-25 22:54:35 +08:00
|
|
|
// only one call can be made in one time
|
|
|
|
if p.download > 1 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-07-24 22:27:08 +08:00
|
|
|
s := spin.New()
|
|
|
|
|
|
|
|
Download:
|
|
|
|
for {
|
2020-08-14 10:43:55 +08:00
|
|
|
|
2020-07-24 23:06:34 +08:00
|
|
|
if gomu.isSuspend {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2020-07-24 22:27:08 +08:00
|
|
|
select {
|
|
|
|
case <-p.done:
|
|
|
|
p.download -= 1
|
|
|
|
if p.download == 0 {
|
|
|
|
p.SetTitle(p.defaultTitle)
|
|
|
|
break Download
|
|
|
|
}
|
|
|
|
case <-time.After(time.Millisecond * 100):
|
2020-07-25 22:53:25 +08:00
|
|
|
|
2020-08-22 14:41:03 +08:00
|
|
|
r, g, b := gomu.colors.accent.RGB()
|
2020-07-25 22:53:25 +08:00
|
|
|
hexColor := padHex(r, g, b)
|
|
|
|
|
|
|
|
title := fmt.Sprintf("─ Playlist ──┤ %d downloads [green]%s[#%s] ├",
|
|
|
|
p.download, s.Next(), hexColor)
|
2020-07-24 22:27:08 +08:00
|
|
|
p.SetTitle(title)
|
|
|
|
gomu.app.Draw()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-07-22 21:00:14 +08:00
|
|
|
// Takes a list of input and suspends tview
|
|
|
|
// returns empty string if cancelled
|
2020-07-23 15:15:39 +08:00
|
|
|
func fzfFind(input []string) (string, error) {
|
2020-07-22 21:00:14 +08:00
|
|
|
|
|
|
|
var in strings.Builder
|
|
|
|
var out strings.Builder
|
|
|
|
|
|
|
|
for _, v := range input {
|
|
|
|
in.WriteString(v + "\n")
|
|
|
|
}
|
|
|
|
|
|
|
|
cmd := exec.Command("fzf")
|
|
|
|
cmd.Stdin = strings.NewReader(in.String())
|
|
|
|
cmd.Stderr = os.Stderr
|
|
|
|
cmd.Stdout = &out
|
|
|
|
|
|
|
|
if err := cmd.Run(); cmd.ProcessState.ExitCode() == 130 {
|
|
|
|
// exit code 130 is when we cancel FZF
|
|
|
|
// not an error
|
|
|
|
return "", nil
|
|
|
|
} else if err != nil {
|
|
|
|
return "", fmt.Errorf("failed to find a file: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
f := strings.TrimSpace(out.String())
|
|
|
|
|
|
|
|
return f, nil
|
|
|
|
}
|
|
|
|
|
2020-07-23 15:15:39 +08:00
|
|
|
// Download audio from youtube audio and adds the song to the selected playlist
|
|
|
|
func ytdl(url string, selPlaylist *tview.TreeNode) error {
|
2020-07-03 11:17:45 +08:00
|
|
|
|
|
|
|
// lookup if youtube-dl exists
|
|
|
|
_, err := exec.LookPath("youtube-dl")
|
|
|
|
|
|
|
|
if err != nil {
|
2020-08-25 20:45:16 +08:00
|
|
|
defaultTimedPopup(" Error ", "youtube-dl is not in your $PATH")
|
2020-07-20 21:48:13 +08:00
|
|
|
|
2020-07-21 12:22:00 +08:00
|
|
|
return tracerr.Wrap(err)
|
2020-07-03 11:17:45 +08:00
|
|
|
}
|
|
|
|
|
2020-07-28 11:24:58 +08:00
|
|
|
dir := viper.GetString("general.music_dir")
|
2020-07-03 11:17:45 +08:00
|
|
|
|
|
|
|
selAudioFile := selPlaylist.GetReference().(*AudioFile)
|
2020-07-23 15:15:39 +08:00
|
|
|
selPlaylistName := selAudioFile.name
|
2020-07-03 11:17:45 +08:00
|
|
|
|
2020-08-25 20:45:16 +08:00
|
|
|
defaultTimedPopup(" Ytdl ", "Downloading")
|
2020-07-03 11:17:45 +08:00
|
|
|
|
|
|
|
// specify the output path for ytdl
|
|
|
|
outputDir := fmt.Sprintf(
|
|
|
|
"%s/%s/%%(title)s.%%(ext)s",
|
|
|
|
dir,
|
|
|
|
selPlaylistName)
|
|
|
|
|
|
|
|
args := []string{
|
|
|
|
"--extract-audio",
|
|
|
|
"--audio-format",
|
|
|
|
"mp3",
|
|
|
|
"--output",
|
|
|
|
outputDir,
|
|
|
|
url,
|
|
|
|
}
|
|
|
|
|
|
|
|
cmd := exec.Command("youtube-dl", args...)
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
|
|
cmd.Stdout = &stdout
|
|
|
|
cmd.Stderr = &stderr
|
|
|
|
|
2020-07-24 22:27:08 +08:00
|
|
|
gomu.playlist.download++
|
|
|
|
go gomu.playlist.updateTitle()
|
|
|
|
|
2020-08-14 10:43:55 +08:00
|
|
|
// blocking
|
2020-07-21 12:22:00 +08:00
|
|
|
err = cmd.Run()
|
2020-07-12 17:05:00 +08:00
|
|
|
|
2020-07-24 22:27:08 +08:00
|
|
|
gomu.playlist.done <- struct{}{}
|
|
|
|
|
2020-07-21 12:22:00 +08:00
|
|
|
if err != nil {
|
2020-08-25 20:45:16 +08:00
|
|
|
defaultTimedPopup(" Error ", "Error running youtube-dl")
|
2020-07-21 12:22:00 +08:00
|
|
|
return tracerr.Wrap(err)
|
|
|
|
}
|
2020-07-12 17:05:00 +08:00
|
|
|
|
2020-07-21 12:22:00 +08:00
|
|
|
playlistPath := path.Join(expandTilde(dir), selPlaylistName)
|
|
|
|
audioPath := extractFilePath(stdout.Bytes(), playlistPath)
|
2020-07-03 11:17:45 +08:00
|
|
|
|
2020-09-25 17:41:58 +08:00
|
|
|
err = appendFile(expandTilde(viper.GetString("general.history_path")), url+"\n")
|
2020-08-18 16:09:43 +08:00
|
|
|
if err != nil {
|
|
|
|
return tracerr.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2020-07-23 15:15:39 +08:00
|
|
|
err = gomu.playlist.addSongToPlaylist(audioPath, selPlaylist)
|
2020-07-21 12:22:00 +08:00
|
|
|
if err != nil {
|
|
|
|
return tracerr.Wrap(err)
|
|
|
|
}
|
2020-07-03 11:17:45 +08:00
|
|
|
|
2020-07-21 12:22:00 +08:00
|
|
|
downloadFinishedMessage := fmt.Sprintf("Finished downloading\n%s",
|
2020-08-15 11:25:57 +08:00
|
|
|
getName(audioPath))
|
2020-07-03 11:17:45 +08:00
|
|
|
|
2020-08-25 20:45:16 +08:00
|
|
|
defaultTimedPopup(" Ytdl ", downloadFinishedMessage)
|
2020-07-13 14:33:13 +08:00
|
|
|
|
2020-07-23 15:15:39 +08:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add songs and their directories in Playlist panel
|
|
|
|
func populate(root *tview.TreeNode, rootPath string) error {
|
|
|
|
|
|
|
|
files, err := ioutil.ReadDir(rootPath)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return tracerr.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, file := range files {
|
|
|
|
|
|
|
|
path := filepath.Join(rootPath, file.Name())
|
|
|
|
f, err := os.Open(path)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
songName := getName(file.Name())
|
|
|
|
child := tview.NewTreeNode(songName)
|
|
|
|
|
|
|
|
if !file.IsDir() {
|
|
|
|
|
|
|
|
filetype, err := getFileContentType(f)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// skip if not mp3 file
|
|
|
|
if filetype != "mpeg" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
audioLength, err := getLength(path)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
audioFile := &AudioFile{
|
|
|
|
name: songName,
|
|
|
|
path: path,
|
|
|
|
isAudioFile: true,
|
|
|
|
length: audioLength,
|
|
|
|
node: child,
|
|
|
|
parent: root,
|
|
|
|
}
|
|
|
|
|
2020-08-11 18:31:33 +08:00
|
|
|
displayText := songName
|
|
|
|
if viper.GetBool("general.emoji") {
|
2020-08-25 21:44:20 +08:00
|
|
|
displayText = fmt.Sprintf(" %s %s",
|
|
|
|
viper.GetString("emoji.file"), songName)
|
2020-08-11 18:31:33 +08:00
|
|
|
}
|
|
|
|
|
2020-07-23 15:15:39 +08:00
|
|
|
child.SetReference(audioFile)
|
2020-08-11 18:31:33 +08:00
|
|
|
child.SetText(displayText)
|
2020-07-23 15:15:39 +08:00
|
|
|
root.AddChild(child)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if file.IsDir() {
|
|
|
|
|
|
|
|
audioFile := &AudioFile{
|
2020-07-24 14:53:17 +08:00
|
|
|
name: songName,
|
|
|
|
path: path,
|
|
|
|
node: child,
|
|
|
|
parent: root,
|
2020-07-23 15:15:39 +08:00
|
|
|
}
|
2020-08-11 18:31:33 +08:00
|
|
|
|
|
|
|
displayText := songName
|
|
|
|
if viper.GetBool("general.emoji") {
|
2020-08-25 21:44:20 +08:00
|
|
|
displayText = fmt.Sprintf(" %s %s",
|
|
|
|
viper.GetString("emoji.playlist"), songName)
|
2020-08-11 18:31:33 +08:00
|
|
|
}
|
|
|
|
|
2020-07-23 15:15:39 +08:00
|
|
|
child.SetReference(audioFile)
|
2020-08-22 14:41:03 +08:00
|
|
|
child.SetColor(gomu.colors.accent)
|
2020-08-11 18:31:33 +08:00
|
|
|
child.SetText(displayText)
|
2020-07-23 15:15:39 +08:00
|
|
|
root.AddChild(child)
|
|
|
|
populate(child, path)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2020-07-03 11:17:45 +08:00
|
|
|
|
2020-07-21 12:22:00 +08:00
|
|
|
return nil
|
2020-07-13 14:33:13 +08:00
|
|
|
}
|