mirror of
https://github.com/issadarkthing/gomu.git
synced 2025-04-26 13:49:21 +08:00
544 lines
10 KiB
Go
544 lines
10 KiB
Go
// Copyright (C) 2020 Raziman
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/gdamore/tcell"
|
|
"github.com/rivo/tview"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
type AudioFile struct {
|
|
Name string
|
|
Path string
|
|
IsAudioFile bool
|
|
Length time.Duration
|
|
Parent *tview.TreeNode
|
|
}
|
|
|
|
type Playlist struct {
|
|
*tview.TreeView
|
|
prevNode *tview.TreeNode
|
|
}
|
|
|
|
func NewPlaylist() *Playlist {
|
|
|
|
rootDir, err := filepath.Abs(expandTilde(viper.GetString("music_dir")))
|
|
|
|
if err != nil {
|
|
appLog(err)
|
|
}
|
|
|
|
root := tview.NewTreeNode(path.Base(rootDir)).
|
|
SetColor(gomu.AccentColor)
|
|
|
|
tree := tview.NewTreeView().SetRoot(root)
|
|
|
|
playlist := &Playlist{tree, nil}
|
|
|
|
rootAudioFile := &AudioFile{
|
|
Name: root.GetText(),
|
|
Path: rootDir,
|
|
IsAudioFile: false,
|
|
Parent: nil,
|
|
}
|
|
|
|
root.SetReference(rootAudioFile)
|
|
|
|
playlist.SetTitle(" Playlist ").SetBorder(true)
|
|
|
|
populate(root, rootDir)
|
|
|
|
var firstChild *tview.TreeNode
|
|
|
|
if len(root.GetChildren()) == 0 {
|
|
firstChild = root
|
|
} else {
|
|
firstChild = root.GetChildren()[0]
|
|
}
|
|
|
|
playlist.SetHighlight(firstChild)
|
|
|
|
playlist.SetChangedFunc(func(node *tview.TreeNode) {
|
|
playlist.SetHighlight(node)
|
|
})
|
|
|
|
playlist.SetSelectedFunc(func(node *tview.TreeNode) {
|
|
node.SetExpanded(!node.IsExpanded())
|
|
})
|
|
|
|
playlist.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey {
|
|
|
|
currNode := playlist.GetCurrentNode()
|
|
|
|
audioFile := currNode.GetReference().(*AudioFile)
|
|
|
|
switch e.Rune() {
|
|
|
|
case 'a':
|
|
|
|
name, _ := gomu.Pages.GetFrontPage()
|
|
|
|
if name != "mkdir-popup" {
|
|
CreatePlaylistPopup()
|
|
}
|
|
|
|
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))
|
|
return
|
|
}
|
|
|
|
err := os.RemoveAll(selectedDir.Path)
|
|
|
|
if err != nil {
|
|
timedPopup(
|
|
" Error ",
|
|
"Unable to delete dir "+selectedDir.Name, getPopupTimeout())
|
|
} else {
|
|
timedPopup(
|
|
" Success ",
|
|
selectedDir.Name+"\nhas been deleted successfully", getPopupTimeout())
|
|
|
|
playlist.Refresh()
|
|
}
|
|
|
|
gomu.Pages.RemovePage("confirmation-popup")
|
|
gomu.App.SetFocus(gomu.PrevPanel.(tview.Primitive))
|
|
|
|
})
|
|
|
|
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))
|
|
return
|
|
}
|
|
|
|
err := os.Remove(audioFile.Path)
|
|
|
|
if err != nil {
|
|
timedPopup(
|
|
" Error ", "Unable to delete "+audioFile.Name, getPopupTimeout())
|
|
} else {
|
|
timedPopup(
|
|
" Success ",
|
|
audioFile.Name+"\nhas been deleted successfully", getPopupTimeout())
|
|
|
|
playlist.Refresh()
|
|
}
|
|
|
|
gomu.Pages.RemovePage("confirmation-popup")
|
|
gomu.App.SetFocus(gomu.PrevPanel.(tview.Primitive))
|
|
})
|
|
|
|
case 'Y':
|
|
|
|
if gomu.Pages.HasPage("download-popup") {
|
|
gomu.Pages.RemovePage("download-popup")
|
|
return e
|
|
}
|
|
|
|
// this ensures it downloads to
|
|
// the correct dir
|
|
if audioFile.IsAudioFile {
|
|
downloadMusicPopup(audioFile.Parent)
|
|
} else {
|
|
downloadMusicPopup(currNode)
|
|
}
|
|
|
|
case 'l':
|
|
|
|
playlist.addToQueue(audioFile)
|
|
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
|
|
|
|
playlist.SetHighlight(parent)
|
|
|
|
parent.SetExpanded(false)
|
|
}
|
|
|
|
currNode.Collapse()
|
|
|
|
case 'L':
|
|
|
|
if !viper.GetBool("confirm_bulk_add") {
|
|
playlist.addAllToQueue(playlist.GetCurrentNode())
|
|
return e
|
|
}
|
|
|
|
confirmationPopup(
|
|
"Are you sure to add this whole directory into queue?",
|
|
func(_ int, label string) {
|
|
|
|
if label == "yes" {
|
|
playlist.addAllToQueue(playlist.GetCurrentNode())
|
|
}
|
|
|
|
gomu.Pages.RemovePage("confirmation-popup")
|
|
gomu.App.SetFocus(playlist)
|
|
|
|
})
|
|
|
|
case 'r':
|
|
|
|
playlist.Refresh()
|
|
|
|
}
|
|
|
|
return e
|
|
})
|
|
|
|
return playlist
|
|
|
|
}
|
|
|
|
// Add songs and their directories in Playlist panel
|
|
func populate(root *tview.TreeNode, rootPath string) {
|
|
|
|
files, err := ioutil.ReadDir(rootPath)
|
|
|
|
if err != nil {
|
|
appLog(err)
|
|
}
|
|
|
|
|
|
for _, file := range files {
|
|
|
|
path := filepath.Join(rootPath, file.Name())
|
|
f, err := os.Open(path)
|
|
|
|
if err != nil {
|
|
appLog(err)
|
|
continue
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
child := tview.NewTreeNode(file.Name())
|
|
|
|
if !file.IsDir() {
|
|
|
|
filetype, err := GetFileContentType(f)
|
|
|
|
if err != nil {
|
|
appLog(err)
|
|
continue
|
|
}
|
|
|
|
// skip if not mp3 file
|
|
if filetype != "mpeg" {
|
|
continue
|
|
}
|
|
|
|
audioLength, err := GetLength(path)
|
|
|
|
if err != nil {
|
|
appLog(err)
|
|
continue
|
|
}
|
|
|
|
audioFile := &AudioFile{
|
|
Name: file.Name(),
|
|
Path: path,
|
|
IsAudioFile: true,
|
|
Length: audioLength,
|
|
Parent: root,
|
|
}
|
|
|
|
child.SetReference(audioFile)
|
|
|
|
}
|
|
|
|
|
|
if file.IsDir() {
|
|
|
|
audioFile := &AudioFile{
|
|
Name: file.Name(),
|
|
Path: path,
|
|
IsAudioFile: false,
|
|
Length: 0,
|
|
Parent: root,
|
|
}
|
|
child.SetReference(audioFile)
|
|
child.SetColor(gomu.AccentColor)
|
|
populate(child, path)
|
|
|
|
}
|
|
|
|
// this is placed below because if
|
|
// any of the checks above returns error
|
|
// it will not be added to the playlist
|
|
root.AddChild(child)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Add to queue and update queue panel
|
|
func (p *Playlist) addToQueue(audioFile *AudioFile) {
|
|
|
|
if audioFile.IsAudioFile {
|
|
|
|
if !gomu.Player.IsRunning {
|
|
|
|
gomu.Player.IsRunning = true
|
|
|
|
go func() {
|
|
// we dont need the primary text as it will be popped anyway
|
|
gomu.Queue.AddItem("", audioFile.Path, 0, nil)
|
|
gomu.Player.Run()
|
|
}()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
songLength, err := GetLength(audioFile.Path)
|
|
|
|
if err != nil {
|
|
appLog(err)
|
|
}
|
|
|
|
queueItemView := fmt.Sprintf("[ %s ] %s", fmtDuration(songLength), audioFile.Name)
|
|
gomu.Queue.AddItem(queueItemView, audioFile.Path, 0, nil)
|
|
}
|
|
}
|
|
|
|
// Bulk add a playlist to queue
|
|
func (p *Playlist) addAllToQueue(root *tview.TreeNode) {
|
|
|
|
var childrens []*tview.TreeNode
|
|
|
|
childrens = root.GetChildren()
|
|
|
|
// gets the parent if the highlighted item is a file
|
|
if len(childrens) == 0 {
|
|
childrens = root.GetReference().(*AudioFile).Parent.GetChildren()
|
|
}
|
|
|
|
for _, v := range childrens {
|
|
currNode := v.GetReference().(*AudioFile)
|
|
|
|
go gomu.Playlist.addToQueue(currNode)
|
|
}
|
|
|
|
}
|
|
|
|
// Refresh the playlist and read the whole root music dir
|
|
func (p *Playlist) Refresh() {
|
|
|
|
root := gomu.Playlist.GetRoot()
|
|
|
|
prevFileName := gomu.Playlist.GetCurrentNode().GetText()
|
|
|
|
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 {
|
|
p.SetHighlight(node)
|
|
return false
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
}
|
|
|
|
// Adds child while setting reference to audio file
|
|
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()
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
// Creates a directory under selected node, returns error if playlist exists
|
|
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)
|
|
|
|
err := os.Mkdir(path.Join(audioFile.Path, name), 0744)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
p.Refresh()
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
// This is used to replace default behaviour of SetCurrentNode which
|
|
// adds color highlight attributes
|
|
func (p *Playlist) SetHighlight(currNode *tview.TreeNode) {
|
|
|
|
if p.prevNode != nil {
|
|
p.prevNode.SetColor(gomu.TextColor)
|
|
}
|
|
currNode.SetColor(gomu.AccentColor)
|
|
p.SetCurrentNode(currNode)
|
|
|
|
if currNode.GetReference().(*AudioFile).IsAudioFile {
|
|
p.prevNode = currNode
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
|
timedPopup(" Error ", "youtube-dl is not in your $PATH", getPopupTimeout())
|
|
return
|
|
}
|
|
|
|
dir := viper.GetString("music_dir")
|
|
|
|
selAudioFile := selPlaylist.GetReference().(*AudioFile)
|
|
selPlaylistName := selAudioFile.Name
|
|
|
|
timedPopup(" Ytdl ", "Downloading", getPopupTimeout())
|
|
|
|
// 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 {
|
|
timedPopup(" Error ", "Error running youtube-dl", getPopupTimeout())
|
|
return
|
|
}
|
|
|
|
playlistPath := path.Join(expandTilde(dir), selPlaylistName)
|
|
|
|
downloadedAudioPath := downloadedFilePath(
|
|
stdout.Bytes(), playlistPath)
|
|
|
|
err = gomu.Playlist.AddSongToPlaylist(downloadedAudioPath, selPlaylist)
|
|
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
|
|
downloadFinishedMessage := fmt.Sprintf("Finished downloading\n%s",
|
|
path.Base(downloadedAudioPath))
|
|
|
|
timedPopup(
|
|
" Ytdl ",
|
|
downloadFinishedMessage,
|
|
getPopupTimeout(),
|
|
)
|
|
|
|
gomu.App.Draw()
|
|
|
|
}()
|
|
|
|
}
|