gomu/playlist.go

570 lines
10 KiB
Go
Raw Normal View History

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-07-03 11:17:45 +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-06-28 13:41:14 +08:00
"time"
2020-06-19 16:22:20 +08:00
"github.com/gdamore/tcell"
"github.com/rivo/tview"
2020-06-24 20:09:47 +08:00
"github.com/spf13/viper"
2020-06-19 16:22:20 +08:00
)
type AudioFile struct {
2020-06-19 16:42:30 +08:00
Name string
Path string
2020-06-19 16:22:20 +08:00
IsAudioFile bool
2020-07-01 17:47:52 +08:00
Length time.Duration
2020-06-19 16:42:30 +08:00
Parent *tview.TreeNode
2020-06-19 16:22:20 +08:00
}
2020-06-26 12:54:48 +08:00
type Playlist struct {
*tview.TreeView
2020-06-28 13:41:14 +08:00
prevNode *tview.TreeNode
2020-06-26 12:54:48 +08:00
}
2020-06-19 16:22:20 +08:00
func NewPlaylist() *Playlist {
2020-06-24 20:09:47 +08:00
2020-06-25 10:46:45 +08:00
rootDir, err := filepath.Abs(expandTilde(viper.GetString("music_dir")))
2020-06-19 16:22:20 +08:00
if err != nil {
2020-07-02 16:11:10 +08:00
appLog(err)
2020-06-19 16:22:20 +08:00
}
2020-07-01 21:21:09 +08:00
root := tview.NewTreeNode(path.Base(rootDir)).
SetColor(gomu.AccentColor)
2020-06-19 16:22:20 +08:00
tree := tview.NewTreeView().SetRoot(root)
2020-06-28 13:41:14 +08:00
2020-07-01 17:48:33 +08:00
playlist := &Playlist{tree, nil}
2020-06-28 13:41:14 +08:00
rootAudioFile := &AudioFile{
2020-07-01 17:48:33 +08:00
Name: root.GetText(),
Path: rootDir,
IsAudioFile: false,
Parent: nil,
2020-06-28 13:41:14 +08:00
}
root.SetReference(rootAudioFile)
2020-06-19 16:22:20 +08:00
2020-06-26 12:54:48 +08:00
playlist.SetTitle(" Playlist ").SetBorder(true)
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-01 21:21:09 +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-01 21:21:09 +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 {
currNode := playlist.GetCurrentNode()
audioFile := currNode.GetReference().(*AudioFile)
switch e.Rune() {
2020-06-28 13:41:14 +08:00
2020-07-01 17:47:52 +08:00
case 'a':
name, _ := gomu.Pages.GetFrontPage()
2020-07-01 17:47:52 +08:00
if name != "mkdir-popup" {
CreatePlaylistPopup()
}
2020-06-28 13:41:14 +08:00
case 'D':
var selectedDir *AudioFile
// gets the parent dir if current focused node is not a dir
if audioFile.IsAudioFile {
selectedDir = audioFile.Parent.GetReference().(*AudioFile)
} else {
selectedDir = audioFile
}
confirmationPopup(
"Are you sure to delete this directory?", func(_ int, buttonName string) {
if buttonName == "no" {
gomu.Pages.RemovePage("confirmation-popup")
gomu.App.SetFocus(gomu.PrevPanel.(tview.Primitive))
2020-06-28 13:41:14 +08:00
return
}
err := os.RemoveAll(selectedDir.Path)
if err != nil {
2020-06-28 14:03:30 +08:00
timedPopup(
2020-06-28 13:41:14 +08:00
" Error ",
2020-07-04 09:47:41 +08:00
"Unable to delete dir "+selectedDir.Name, getPopupTimeout())
2020-06-28 13:41:14 +08:00
} else {
2020-06-28 14:03:30 +08:00
timedPopup(
2020-06-28 13:41:14 +08:00
" Success ",
2020-07-04 09:47:41 +08:00
selectedDir.Name+"\nhas been deleted successfully", getPopupTimeout())
2020-06-28 13:41:14 +08:00
playlist.Refresh()
}
gomu.Pages.RemovePage("confirmation-popup")
gomu.App.SetFocus(gomu.PrevPanel.(tview.Primitive))
2020-06-28 13:41:14 +08:00
})
case 'd':
// prevent from deleting a directory
if !audioFile.IsAudioFile {
return e
}
confirmationPopup(
"Are you sure to delete this audio file?", func(_ int, buttonName string) {
if buttonName == "no" {
gomu.Pages.RemovePage("confirmation-popup")
gomu.App.SetFocus(gomu.PrevPanel.(tview.Primitive))
2020-06-28 13:41:14 +08:00
return
}
err := os.Remove(audioFile.Path)
if err != nil {
2020-06-28 14:03:30 +08:00
timedPopup(
2020-07-04 09:47:41 +08:00
" Error ", "Unable to delete "+audioFile.Name, getPopupTimeout())
2020-06-28 13:41:14 +08:00
} else {
2020-06-28 14:03:30 +08:00
timedPopup(
2020-06-28 13:41:14 +08:00
" Success ",
2020-07-04 09:47:41 +08:00
audioFile.Name+"\nhas been deleted successfully", getPopupTimeout())
2020-06-28 13:41:14 +08:00
playlist.Refresh()
}
gomu.Pages.RemovePage("confirmation-popup")
gomu.App.SetFocus(gomu.PrevPanel.(tview.Primitive))
2020-06-28 13:41:14 +08:00
})
2020-06-27 17:16:49 +08:00
case 'Y':
if gomu.Pages.HasPage("download-popup") {
gomu.Pages.RemovePage("download-popup")
2020-06-28 13:41:14 +08:00
return e
}
2020-06-27 17:16:49 +08:00
2020-06-28 13:41:14 +08:00
// this ensures it downloads to
// the correct dir
if audioFile.IsAudioFile {
2020-07-02 16:11:10 +08:00
downloadMusicPopup(audioFile.Parent)
2020-06-28 13:41:14 +08:00
} else {
2020-07-02 16:11:10 +08:00
downloadMusicPopup(currNode)
2020-06-27 17:16:49 +08:00
}
2020-06-26 12:54:48 +08:00
case 'l':
2020-07-06 18:58:27 +08:00
playlist.AddToQueue(audioFile)
2020-06-26 12:54:48 +08:00
currNode.SetExpanded(true)
case 'h':
// if closing node with no children
// close the node's parent
// remove the color of the node
if audioFile.IsAudioFile {
parent := audioFile.Parent
2020-07-01 21:21:09 +08:00
playlist.SetHighlight(parent)
2020-06-26 12:54:48 +08:00
parent.SetExpanded(false)
}
currNode.Collapse()
case 'L':
2020-06-26 17:09:15 +08:00
if !viper.GetBool("confirm_bulk_add") {
2020-07-06 18:58:27 +08:00
playlist.AddAllToQueue(playlist.GetCurrentNode())
2020-06-26 17:09:15 +08:00
return e
}
2020-06-26 12:54:48 +08:00
confirmationPopup(
"Are you sure to add this whole directory into queue?",
func(_ int, label string) {
if label == "yes" {
2020-07-06 18:58:27 +08:00
playlist.AddAllToQueue(playlist.GetCurrentNode())
2020-06-26 12:54:48 +08:00
}
gomu.Pages.RemovePage("confirmation-popup")
gomu.App.SetFocus(playlist)
2020-06-26 12:54:48 +08:00
})
2020-07-02 16:11:10 +08:00
case 'r':
playlist.Refresh()
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
}
// Add songs and their directories in Playlist panel
2020-06-19 16:22:20 +08:00
func populate(root *tview.TreeNode, rootPath string) {
files, err := ioutil.ReadDir(rootPath)
if err != nil {
2020-07-02 16:11:10 +08:00
appLog(err)
2020-06-19 16:22:20 +08:00
}
2020-06-19 16:22:20 +08:00
for _, file := range files {
path := filepath.Join(rootPath, file.Name())
2020-06-23 18:42:27 +08:00
f, err := os.Open(path)
if err != nil {
2020-07-02 16:11:10 +08:00
appLog(err)
continue
2020-06-23 18:42:27 +08:00
}
defer f.Close()
2020-07-01 17:47:52 +08:00
child := tview.NewTreeNode(file.Name())
2020-06-23 18:42:27 +08:00
if !file.IsDir() {
filetype, err := GetFileContentType(f)
if err != nil {
2020-07-02 16:11:10 +08:00
appLog(err)
continue
2020-06-23 18:42:27 +08:00
}
// skip if not mp3 file
if filetype != "mpeg" {
continue
}
2020-07-01 17:47:52 +08:00
audioLength, err := GetLength(path)
2020-06-23 18:42:27 +08:00
2020-07-01 17:47:52 +08:00
if err != nil {
2020-07-02 16:11:10 +08:00
appLog(err)
continue
2020-07-01 17:47:52 +08:00
}
2020-06-19 16:22:20 +08:00
2020-07-01 17:47:52 +08:00
audioFile := &AudioFile{
Name: file.Name(),
Path: path,
IsAudioFile: true,
Length: audioLength,
Parent: root,
}
2020-06-19 16:22:20 +08:00
2020-07-01 17:47:52 +08:00
child.SetReference(audioFile)
}
2020-06-19 16:22:20 +08:00
2020-06-19 16:22:20 +08:00
if file.IsDir() {
2020-07-01 17:47:52 +08:00
audioFile := &AudioFile{
Name: file.Name(),
Path: path,
IsAudioFile: false,
Length: 0,
Parent: root,
}
child.SetReference(audioFile)
child.SetColor(gomu.AccentColor)
2020-06-19 16:22:20 +08:00
populate(child, path)
2020-07-01 17:47:52 +08:00
2020-06-19 16:22:20 +08:00
}
// this is placed below because if
// any of the checks above returns error
// it will not be added to the playlist
root.AddChild(child)
2020-06-19 16:22:20 +08:00
}
}
2020-06-25 10:46:45 +08:00
// Add to queue and update queue panel
2020-07-06 18:58:27 +08:00
func (p *Playlist) AddToQueue(audioFile *AudioFile) {
2020-06-25 10:46:45 +08:00
2020-06-25 14:12:19 +08:00
if audioFile.IsAudioFile {
2020-06-25 10:46:45 +08:00
if !gomu.Player.IsRunning {
2020-06-25 14:12:19 +08:00
gomu.Player.IsRunning = true
2020-06-25 14:12:19 +08:00
2020-06-26 12:54:48 +08:00
go func() {
// we dont need the primary text as it will be popped anyway
gomu.Queue.AddItem("", audioFile.Path, 0, nil)
gomu.Player.Run()
2020-06-26 12:54:48 +08:00
}()
2020-06-25 14:12:19 +08:00
2020-07-03 00:50:32 +08:00
return
2020-06-25 14:12:19 +08:00
2020-07-03 00:50:32 +08:00
}
2020-06-25 14:12:19 +08:00
2020-07-01 21:21:09 +08:00
songLength, err := GetLength(audioFile.Path)
2020-06-26 12:54:48 +08:00
2020-07-01 21:21:09 +08:00
if err != nil {
2020-07-02 16:11:10 +08:00
appLog(err)
2020-06-26 12:54:48 +08:00
}
2020-07-01 21:21:09 +08:00
queueItemView := fmt.Sprintf("[ %s ] %s", fmtDuration(songLength), audioFile.Name)
gomu.Queue.AddItem(queueItemView, audioFile.Path, 0, nil)
2020-06-25 10:46:45 +08:00
}
2020-06-25 14:12:19 +08:00
}
// Bulk add a playlist to queue
2020-07-06 18:58:27 +08:00
func (p *Playlist) AddAllToQueue(root *tview.TreeNode) {
2020-06-25 14:12:19 +08:00
var childrens []*tview.TreeNode
2020-06-25 10:46:45 +08:00
2020-06-25 14:12:19 +08:00
childrens = root.GetChildren()
2020-07-01 21:21:09 +08:00
// gets the parent if the highlighted item is a file
2020-06-25 14:12:19 +08:00
if len(childrens) == 0 {
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-06 18:58:27 +08:00
gomu.Playlist.AddToQueue(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
// Refresh the playlist and read the whole root music dir
2020-07-01 17:47:52 +08:00
func (p *Playlist) Refresh() {
2020-06-28 13:41:14 +08:00
root := gomu.Playlist.GetRoot()
2020-06-28 13:41:14 +08:00
prevFileName := gomu.Playlist.GetCurrentNode().GetText()
2020-06-28 13:41:14 +08:00
root.ClearChildren()
populate(root, root.GetReference().(*AudioFile).Path)
root.Walk(func(node, parent *tview.TreeNode) bool {
// to preserve previously highlighted node
if node.GetReference().(*AudioFile).Name == prevFileName {
2020-07-01 21:21:09 +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-01 17:47:52 +08:00
func (p *Playlist) AddSongToPlaylist(audioPath string, selPlaylist *tview.TreeNode) error {
f, err := os.Open(audioPath)
if err != nil {
return err
}
defer f.Close()
node := tview.NewTreeNode(path.Base(audioPath))
audioLength, err := GetLength(audioPath)
if err != nil {
return err
}
audioFile := &AudioFile{
Name: path.Base(audioPath),
Path: audioPath,
IsAudioFile: true,
Length: audioLength,
Parent: selPlaylist,
}
node.SetReference(audioFile)
selPlaylist.AddChild(node)
gomu.App.Draw()
2020-07-01 17:47:52 +08:00
return nil
}
// Creates a directory under selected node, returns error if playlist exists
2020-07-01 17:47:52 +08:00
func (p *Playlist) CreatePlaylist(name string) error {
selectedNode := p.GetCurrentNode()
parentNode := selectedNode.GetReference().(*AudioFile).Parent
// if the current node is the root
// sets the parent to itself
if parentNode == nil {
parentNode = selectedNode
}
audioFile := parentNode.GetReference().(*AudioFile)
2020-07-06 17:02:47 +08:00
err := os.Mkdir(path.Join(audioFile.Path, name), 0744)
2020-07-01 17:47:52 +08:00
if err != nil {
return err
}
p.Refresh()
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
func (p *Playlist) SetHighlight(currNode *tview.TreeNode) {
if p.prevNode != nil {
p.prevNode.SetColor(gomu.TextColor)
2020-07-01 21:21:09 +08:00
}
currNode.SetColor(gomu.AccentColor)
2020-07-01 21:21:09 +08:00
p.SetCurrentNode(currNode)
if currNode.GetReference().(*AudioFile).IsAudioFile {
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
func (p *Playlist) FindAudioFile(audioName string) *AudioFile {
root := p.GetRoot()
if root == nil {
return nil
}
var selNode *AudioFile
root.Walk(func(node, _ *tview.TreeNode) bool {
audioFile := node.GetReference().(*AudioFile)
if audioFile.Name == audioName {
selNode = audioFile
return false
}
return true
})
return selNode
}
2020-07-03 11:17:45 +08:00
// download audio from youtube audio and adds the song to the selected playlist
func Ytdl(url string, selPlaylist *tview.TreeNode) {
// lookup if youtube-dl exists
_, err := exec.LookPath("youtube-dl")
if err != nil {
2020-07-04 09:47:41 +08:00
timedPopup(" Error ", "youtube-dl is not in your $PATH", getPopupTimeout())
2020-07-03 11:17:45 +08:00
return
}
dir := viper.GetString("music_dir")
selAudioFile := selPlaylist.GetReference().(*AudioFile)
selPlaylistName := selAudioFile.Name
2020-07-04 09:47:41 +08:00
timedPopup(" Ytdl ", "Downloading", getPopupTimeout())
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
go func() {
err := cmd.Run()
if err != nil {
2020-07-04 09:47:41 +08:00
timedPopup(" Error ", "Error running youtube-dl", getPopupTimeout())
2020-07-03 11:17:45 +08:00
return
}
playlistPath := path.Join(expandTilde(dir), selPlaylistName)
downloadedAudioPath := downloadedFilePath(
stdout.Bytes(), playlistPath)
err = gomu.Playlist.AddSongToPlaylist(downloadedAudioPath, selPlaylist)
2020-07-03 11:17:45 +08:00
if err != nil {
log.Println(err)
}
downloadFinishedMessage := fmt.Sprintf("Finished downloading\n%s",
path.Base(downloadedAudioPath))
timedPopup(
" Ytdl ",
downloadFinishedMessage,
2020-07-04 09:47:41 +08:00
getPopupTimeout(),
2020-07-03 11:17:45 +08:00
)
gomu.App.Draw()
2020-07-03 11:17:45 +08:00
}()
}