gomu/playlist.go

831 lines
18 KiB
Go
Raw Normal View History

//go:build !darwin
// +build !darwin
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"
2021-04-01 15:24:31 +08:00
"errors"
2020-06-23 22:27:02 +08:00
"fmt"
2020-06-19 16:22:20 +08:00
"io/ioutil"
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"
2021-03-14 20:32:02 +08:00
"sort"
2020-07-22 21:01:13 +08:00
"strings"
2021-03-14 20:32:02 +08:00
"syscall"
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-07-24 22:27:08 +08:00
spin "github.com/tj/go-spin"
"github.com/tramhao/id3v2"
2020-07-21 12:22:00 +08:00
"github.com/ztrue/tracerr"
"github.com/issadarkthing/gomu/lyric"
2021-03-21 16:05:23 +08:00
"github.com/issadarkthing/gomu/player"
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{}
yankFile *player.AudioFile
2021-04-01 15:24:31 +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",
2021-02-04 21:27:17 +08:00
"Y download audio from url",
2020-07-17 15:34:50 +08:00
"r refresh",
2020-08-18 16:09:43 +08:00
"R rename",
"y/p yank/paste file",
2020-07-24 23:06:34 +08:00
"/ find in playlist",
"s search audio from youtube",
2021-02-19 14:28:55 +08:00
"t edit mp3 tags",
2021-03-09 01:48:01 +08:00
"1/2 find lyric if available",
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
2021-02-14 13:45:43 +08:00
anko := gomu.anko
2021-02-18 10:23:15 +08:00
m := anko.GetString("General.music_dir")
2021-02-14 13:45:43 +08:00
rootDir, err := filepath.Abs(expandTilde(m))
if err != nil {
2021-02-14 13:45:43 +08:00
err = tracerr.Errorf("unable to find music directory: %e", err)
die(err)
}
2020-08-11 13:39:02 +08:00
// if not default value was given
if *args.music != "~/music" {
rootDir = expandFilePath(*args.music)
}
var rootTextView string
2021-02-18 10:23:15 +08:00
useEmoji := anko.GetBool("General.use_emoji")
2021-02-14 13:45:43 +08:00
if useEmoji {
2021-02-18 10:23:15 +08:00
emojiPlaylist := anko.GetString("Emoji.playlist")
2021-02-14 13:45:43 +08:00
rootTextView = fmt.Sprintf("%s %s", emojiPlaylist, path.Base(rootDir))
} else {
rootTextView = path.Base(rootDir)
}
2020-08-25 22:05:17 +08:00
root := tview.NewTreeNode(rootTextView).
2021-03-16 11:47:35 +08:00
SetColor(gomu.colors.playlistDir)
2020-06-19 16:22:20 +08:00
tree := tview.NewTreeView().SetRoot(root)
2021-03-14 18:20:48 +08:00
tree.SetBackgroundColor(gomu.colors.background)
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 := new(player.AudioFile)
rootAudioFile.SetName(path.Base(rootDir))
rootAudioFile.SetNode(root)
rootAudioFile.SetPath(rootDir)
2020-06-28 13:41:14 +08:00
root.SetReference(rootAudioFile)
2021-03-16 11:47:35 +08:00
root.SetColor(gomu.colors.playlistDir)
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
2021-03-15 11:33:27 +08:00
populate(root, rootDir, gomu.anko.GetBool("General.sort_by_mtime"))
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
2021-03-17 21:46:23 +08:00
cmds := map[rune]string{
'a': "create_playlist",
'D': "delete_playlist",
'd': "delete_file",
'Y': "download_audio",
's': "youtube_search",
'l': "add_queue",
'L': "bulk_add",
'h': "close_node",
'r': "refresh",
'R': "rename",
'y': "yank",
'p': "paste",
'/': "playlist_search",
't': "edit_tags",
'1': "fetch_lyric",
'2': "fetch_lyric_cn2",
}
for key, cmdName := range cmds {
src := fmt.Sprintf(`Keybinds.def_p("%c", %s)`, key, cmdName)
2021-03-19 23:10:16 +08:00
anko.Execute(src)
2021-03-17 21:46:23 +08:00
}
2020-06-26 12:54:48 +08:00
playlist.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey {
if gomu.anko.KeybindExists("playlist", e) {
2021-02-14 22:15:31 +08:00
err := gomu.anko.ExecKeybind("playlist", e)
if err != nil {
errorPopup(err)
}
2021-02-14 22:15:31 +08:00
return nil
2021-02-14 22:15:31 +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
}
// Returns the current file highlighted in the playlist
func (p Playlist) getCurrentFile() *player.AudioFile {
node := p.GetCurrentNode()
if node == nil {
return nil
}
return node.GetReference().(*player.AudioFile)
}
2020-07-23 15:34:56 +08:00
// Deletes song from filesystem
func (p *Playlist) deleteSong(audioFile *player.AudioFile) {
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
}
// hehe we need to move focus to next node before delete it
p.InputHandler()(tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone), nil)
2020-07-20 21:48:13 +08:00
err := os.Remove(audioFile.Path())
2020-07-20 21:48:13 +08:00
if err != nil {
errorPopup(err)
return
2020-07-20 21:48:13 +08:00
}
defaultTimedPopup(" Success ",
audioFile.Name()+"\nhas been deleted successfully")
go gomu.app.QueueUpdateDraw(func() {
p.refresh()
// Here we remove the song from queue
gomu.queue.updateQueuePath()
gomu.queue.updateCurrentSongDelete(audioFile)
})
2020-07-21 01:21:59 +08:00
})
2020-07-20 21:48:13 +08:00
}
// Deletes playlist/dir from filesystem
func (p *Playlist) deletePlaylist(audioFile *player.AudioFile) (err error) {
2020-07-20 21:48:13 +08:00
// here we close the node and then move to next folder before delete
p.InputHandler()(tcell.NewEventKey(tcell.KeyRune, 'h', tcell.ModNone), nil)
p.InputHandler()(tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone), nil)
err = os.RemoveAll(audioFile.Path())
2021-03-02 14:50:25 +08:00
if err != nil {
return tracerr.Wrap(err)
2021-03-02 14:50:25 +08:00
}
2020-07-20 21:48:13 +08:00
defaultTimedPopup(
" Success ",
audioFile.Name()+"\nhas been deleted successfully")
go gomu.app.QueueUpdateDraw(func() {
p.refresh()
// Here we remove the song from queue
gomu.queue.updateQueuePath()
gomu.queue.updateCurrentSongDelete(audioFile)
})
return nil
2020-07-20 21:48:13 +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
if root.GetReference().(*player.AudioFile).IsAudioFile() {
childrens = root.GetReference().(*player.AudioFile).ParentNode().GetChildren()
2020-06-26 12:54:48 +08:00
}
2020-06-25 14:12:19 +08:00
for _, v := range childrens {
currNode := v.GetReference().(*player.AudioFile)
if currNode.IsAudioFile() {
2021-03-02 14:50:25 +08:00
2021-02-26 11:18:58 +08:00
currSong := gomu.player.GetCurrentSong()
if currSong == nil || (currNode.Name() != currSong.Name()) {
gomu.queue.enqueue(currNode)
}
2021-02-26 11:18:58 +08:00
2021-02-02 22:52:37 +08:00
}
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()
2021-03-21 20:45:07 +08:00
prevNode := gomu.playlist.GetCurrentNode()
prevFilepath := prevNode.GetReference().(*player.AudioFile).Path()
2020-06-28 13:41:14 +08:00
root.ClearChildren()
node := root.GetReference().(*player.AudioFile)
2020-06-28 13:41:14 +08:00
populate(root, node.Path(), gomu.anko.GetBool("General.sort_by_mtime"))
2020-06-28 13:41:14 +08:00
2021-02-04 12:23:32 +08:00
root.Walk(func(node, _ *tview.TreeNode) bool {
2020-06-28 13:41:14 +08:00
// to preserve previously highlighted node
if node.GetReference().(*player.AudioFile).Path() == prevFilepath {
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
}
// 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
2021-03-13 01:13:45 +08:00
// populateAudioLength(selPlaylist)
audioLength, err := getTagLength(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 := new(player.AudioFile)
audioFile.SetName(songName)
audioFile.SetPath(audioPath)
audioFile.SetIsAudioFile(true)
audioFile.SetLen(audioLength)
audioFile.SetNode(node)
audioFile.SetParentNode(selPlaylist)
2020-07-01 17:47:52 +08:00
2021-02-26 15:31:55 +08:00
displayText := setDisplayText(audioFile)
2020-08-11 18:31:33 +08:00
2020-07-01 17:47:52 +08:00
node.SetReference(audioFile)
2020-08-11 18:31:33 +08:00
node.SetText(displayText)
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
func (p *Playlist) getAudioFiles() []*player.AudioFile {
2020-07-10 22:05:36 +08:00
2020-07-10 16:52:37 +08:00
root := p.GetRoot()
audioFiles := []*player.AudioFile{}
2020-07-10 16:52:37 +08:00
root.Walk(func(node, _ *tview.TreeNode) bool {
audioFile := node.GetReference().(*player.AudioFile)
2020-07-10 16:52:37 +08:00
audioFiles = append(audioFiles, audioFile)
2020-07-10 22:05:36 +08:00
2020-07-10 16:52:37 +08:00
return true
})
return audioFiles
}
// 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()
parentNode := selectedNode.GetReference().(*player.AudioFile).ParentNode()
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().(*player.AudioFile)
2020-07-01 17:47:52 +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
// 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 {
if p.prevNode.GetReference().(*player.AudioFile).IsAudioFile() {
2021-03-16 11:47:35 +08:00
p.prevNode.SetColor(gomu.colors.foreground)
} else {
p.prevNode.SetColor(gomu.colors.playlistDir)
}
2020-07-01 21:21:09 +08:00
}
2021-03-16 11:47:35 +08:00
currNode.SetColor(gomu.colors.playlistHi)
p.SetCurrentNode(currNode)
2020-07-01 21:21:09 +08:00
2021-03-16 11:47:35 +08:00
p.prevNode = currNode
2020-07-01 21:21:09 +08:00
}
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
func (p *Playlist) findAudioFile(audioName string) (*player.AudioFile, error) {
2020-07-06 18:58:27 +08:00
root := p.GetRoot()
var selNode *player.AudioFile
2020-07-06 18:58:27 +08:00
root.Walk(func(node, _ *tview.TreeNode) bool {
audioFile := node.GetReference().(*player.AudioFile)
2020-07-06 18:58:27 +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-08-18 16:09:43 +08:00
func (p *Playlist) rename(newName string) error {
currentNode := p.GetCurrentNode()
audio := currentNode.GetReference().(*player.AudioFile)
2021-04-01 15:24:31 +08:00
pathToFile, _ := filepath.Split(audio.Path())
2020-08-18 16:09:43 +08:00
var newPath string
if audio.IsAudioFile() {
2020-08-18 16:09:43 +08:00
newPath = pathToFile + newName + ".mp3"
} else {
newPath = pathToFile + newName
}
err := os.Rename(audio.Path(), newPath)
2020-08-18 16:09:43 +08:00
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
}
// 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 22:27:08 +08:00
select {
case <-p.done:
2021-03-25 12:01:12 +08:00
p.download--
2020-07-24 22:27:08 +08:00
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-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
}
selAudioFile := selPlaylist.GetReference().(*player.AudioFile)
dir := selAudioFile.Path()
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/%%(title)s.%%(ext)s",
2021-02-04 15:35:58 +08:00
dir)
2020-07-03 11:17:45 +08:00
2021-02-19 14:28:55 +08:00
metaData := fmt.Sprintf("%%(artist)s - %%(title)s")
2020-07-03 11:17:45 +08:00
args := []string{
"--extract-audio",
"--audio-format",
"mp3",
"--output",
outputDir,
2021-02-19 14:28:55 +08:00
"--add-metadata",
"--embed-thumbnail",
"--metadata-from-title",
metaData,
2021-02-21 15:18:19 +08:00
"--write-sub",
"--all-subs",
2021-02-21 15:18:19 +08:00
"--convert-subs",
"lrc",
2020-07-03 11:17:45 +08:00
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 {
return tracerr.Wrap(err)
}
2020-07-12 17:05:00 +08:00
playlistPath := dir
2020-07-21 12:22:00 +08:00
audioPath := extractFilePath(stdout.Bytes(), playlistPath)
2020-07-03 11:17:45 +08:00
2021-02-18 10:23:15 +08:00
historyPath := gomu.anko.GetString("General.history_path")
2021-02-14 13:45:43 +08:00
err = appendFile(expandTilde(historyPath), 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
2021-02-24 10:32:29 +08:00
// Embed lyric to mp3 as uslt
var tag *id3v2.Tag
tag, err = id3v2.Open(audioPath, id3v2.Options{Parse: true})
if err != nil {
return tracerr.Wrap(err)
}
defer tag.Close()
pathToFile, _ := filepath.Split(audioPath)
files, err := ioutil.ReadDir(pathToFile)
if err != nil {
logError(err)
}
var lyricWritten int = 0
for _, file := range files {
fileName := file.Name()
fileExt := filepath.Ext(fileName)
lyricFileName := filepath.Join(pathToFile, fileName)
if fileExt == ".lrc" {
2021-02-24 10:32:29 +08:00
// Embed all lyrics and use langExt as content descriptor of uslt
fileNameWithoutExt := strings.TrimSuffix(fileName, fileExt)
langExt := strings.TrimPrefix(filepath.Ext(fileNameWithoutExt), ".")
// Read entire file content, giving us little control but
// making it very simple. No need to close the file.
byteContent, err := ioutil.ReadFile(lyricFileName)
if err != nil {
return tracerr.Wrap(err)
}
lyricContent := string(byteContent)
2021-03-25 16:57:44 +08:00
var lyric lyric.Lyric
err = lyric.NewFromLRC(lyricContent)
if err != nil {
return tracerr.Wrap(err)
}
lyric.LangExt = langExt
err = embedLyric(audioPath, &lyric, false)
if err != nil {
return tracerr.Wrap(err)
}
err = os.Remove(lyricFileName)
if err != nil {
return tracerr.Wrap(err)
}
lyricWritten++
}
}
downloadFinishedMessage := fmt.Sprintf("Finished downloading\n%s\n%v lyrics embeded", getName(audioPath), lyricWritten)
2020-08-25 20:45:16 +08:00
defaultTimedPopup(" Ytdl ", downloadFinishedMessage)
2021-02-04 15:35:58 +08:00
gomu.app.Draw()
2020-07-13 14:33:13 +08:00
2020-07-23 15:15:39 +08:00
return nil
}
2021-03-14 20:32:02 +08:00
// Add songs and their directories in Playlist panel.
2021-03-15 11:33:27 +08:00
func populate(root *tview.TreeNode, rootPath string, sortMtime bool) error {
2020-07-23 15:15:39 +08:00
files, err := ioutil.ReadDir(rootPath)
if err != nil {
return tracerr.Wrap(err)
}
2021-03-15 11:33:27 +08:00
if sortMtime {
2021-03-14 20:32:02 +08:00
sort.Slice(files, func(i, j int) bool {
stat1 := files[i].Sys().(*syscall.Stat_t)
stat2 := files[j].Sys().(*syscall.Stat_t)
2021-03-15 11:33:27 +08:00
time1 := time.Unix(stat1.Mtim.Unix())
time2 := time.Unix(stat2.Mtim.Unix())
2021-03-14 20:32:02 +08:00
return time1.After(time2)
})
}
2020-07-23 15:15:39 +08:00
for _, file := range files {
path, err := filepath.EvalSymlinks(filepath.Join(rootPath, file.Name()))
2020-07-23 15:15:39 +08:00
if err != nil {
continue
}
songName := getName(file.Name())
child := tview.NewTreeNode(songName)
if file.Mode().IsRegular() {
2020-07-23 15:15:39 +08:00
2021-02-02 22:52:37 +08:00
f, err := os.Open(path)
if err != nil {
continue
}
defer f.Close()
2020-07-23 15:15:39 +08:00
filetype, err := getFileContentType(f)
if err != nil {
continue
}
// skip if not mp3 file
if filetype != "mpeg" {
continue
}
audioFile := new(player.AudioFile)
audioFile.SetName(songName)
audioFile.SetPath(path)
audioFile.SetIsAudioFile(true)
audioFile.SetNode(child)
audioFile.SetParentNode(root)
2020-07-23 15:15:39 +08:00
audioLength, err := getTagLength(audioFile.Path())
if err != nil {
logError(err)
}
audioFile.SetLen(audioLength)
2021-02-26 15:31:55 +08:00
displayText := setDisplayText(audioFile)
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)
}
2021-02-04 12:23:32 +08:00
if file.IsDir() || file.Mode()&os.ModeSymlink != 0 {
2020-07-23 15:15:39 +08:00
audioFile := new(player.AudioFile)
audioFile.SetName(songName)
audioFile.SetPath(path)
audioFile.SetIsAudioFile(false)
audioFile.SetNode(child)
audioFile.SetParentNode(root)
2020-08-11 18:31:33 +08:00
2021-02-26 15:31:55 +08:00
displayText := setDisplayText(audioFile)
2020-08-11 18:31:33 +08:00
2020-07-23 15:15:39 +08:00
child.SetReference(audioFile)
2021-03-16 11:47:35 +08:00
child.SetColor(gomu.colors.playlistDir)
2020-08-11 18:31:33 +08:00
child.SetText(displayText)
2020-07-23 15:15:39 +08:00
root.AddChild(child)
2021-03-15 11:33:27 +08:00
populate(child, path, sortMtime)
2020-07-23 15:15:39 +08:00
}
}
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
}
func (p *Playlist) yank() error {
2021-04-01 15:24:31 +08:00
p.yankFile = p.getCurrentFile()
if p.yankFile == nil {
return errors.New("no file has been yanked")
}
if p.yankFile.Node() == p.GetRoot() {
2021-04-01 15:24:31 +08:00
return errors.New("please don't yank the root directory")
}
defaultTimedPopup(" Success ", p.yankFile.Name()+"\n has been yanked successfully.")
2021-02-02 22:52:37 +08:00
return nil
}
func (p *Playlist) paste() error {
2021-04-01 15:24:31 +08:00
if p.yankFile == nil {
return errors.New("no file has been yanked")
}
oldAudio := p.yankFile
oldPathDir, oldPathFileName := filepath.Split(p.yankFile.Path())
2021-04-01 15:24:31 +08:00
pasteFile := p.getCurrentFile()
var newPathDir string
if pasteFile.IsAudioFile() {
newPathDir, _ = filepath.Split(pasteFile.Path())
2021-04-01 15:24:31 +08:00
} else {
newPathDir = pasteFile.Path()
2021-04-01 15:24:31 +08:00
}
2021-04-01 15:24:31 +08:00
if oldPathDir == newPathDir {
return nil
}
2021-02-02 22:52:37 +08:00
2021-04-01 15:24:31 +08:00
newPathFull := filepath.Join(newPathDir, oldPathFileName)
err := os.Rename(p.yankFile.Path(), newPathFull)
2021-04-01 15:24:31 +08:00
if err != nil {
return tracerr.Wrap(err)
2021-02-02 22:52:37 +08:00
}
defaultTimedPopup(" Success ", p.yankFile.Name()+"\n has been pasted to\n"+newPathDir)
2021-04-01 15:24:31 +08:00
// keep queue references updated
newAudio := oldAudio
newAudio.SetPath(newPathFull)
p.refresh()
gomu.queue.updateQueuePath()
if p.yankFile.IsAudioFile() {
err = gomu.queue.updateCurrentSongName(oldAudio, newAudio)
if err != nil {
return tracerr.Wrap(err)
}
} else {
err = gomu.queue.updateCurrentSongPath(oldAudio, newAudio)
if err != nil {
return tracerr.Wrap(err)
}
}
p.yankFile = nil
2021-02-02 22:52:37 +08:00
return nil
}
func setDisplayText(audioFile *player.AudioFile) string {
2021-02-18 10:23:15 +08:00
useEmoji := gomu.anko.GetBool("General.use_emoji")
if !useEmoji {
return audioFile.Name()
}
if audioFile.IsAudioFile() {
2021-02-26 15:31:55 +08:00
emojiFile := gomu.anko.GetString("Emoji.file")
return fmt.Sprintf(" %s %s", emojiFile, audioFile.Name())
}
2021-02-26 15:31:55 +08:00
emojiDir := gomu.anko.GetString("Emoji.playlist")
return fmt.Sprintf(" %s %s", emojiDir, audioFile.Name())
}
2021-04-01 16:09:09 +08:00
// refreshByNode is called after rename of file or folder, to refresh queue info
func (p *Playlist) refreshAfterRename(node *player.AudioFile, newName string) error {
2021-04-01 16:09:09 +08:00
root := p.GetRoot()
root.Walk(func(node, _ *tview.TreeNode) bool {
if strings.Contains(node.GetText(), newName) {
p.setHighlight(node)
}
return true
})
// update queue
newNode := p.getCurrentFile()
if node.IsAudioFile() {
2021-04-06 14:55:40 +08:00
err := gomu.queue.renameItem(node, newNode)
2021-04-01 16:09:09 +08:00
if err != nil {
return tracerr.Wrap(err)
}
err = gomu.queue.updateCurrentSongName(node, newNode)
if err != nil {
return tracerr.Wrap(err)
}
2021-04-01 16:09:09 +08:00
} else {
gomu.queue.updateQueuePath()
err := gomu.queue.updateCurrentSongPath(node, newNode)
if err != nil {
return tracerr.Wrap(err)
}
}
2021-04-01 16:09:09 +08:00
return nil
}