diff --git a/command.go b/command.go index f8e1140..da6154f 100644 --- a/command.go +++ b/command.go @@ -475,10 +475,17 @@ func (c Command) defineCommands() { replPopup() }) + c.define("edit_tags", func() { + audioFile := gomu.playlist.getCurrentFile() + tagPopup(audioFile) + + }) + for name, cmd := range c.commands { err := gomu.anko.Define(name, cmd) if err != nil { logError(err) } } + } diff --git a/go.mod b/go.mod index 6cf768e..cfcbc57 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/issadarkthing/gomu go 1.14 require ( + github.com/bogem/id3v2 v1.2.0 github.com/faiface/beep v1.0.2 github.com/gdamore/tcell/v2 v2.1.0 github.com/hajimehoshi/go-mp3 v0.3.1 // indirect diff --git a/go.sum b/go.sum index fe50f44..d8a2a30 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/bogem/id3v2 v1.2.0 h1:hKDF+F1gOgQ5r1QmBCEZUk4MveJbKxCeIDSBU7CQ4oI= +github.com/bogem/id3v2 v1.2.0/go.mod h1:t78PK5AQ56Q47kizpYiV6gtjj3jfxlz87oFpty8DYs8= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/faiface/beep v1.0.2 h1:UB5DiRNmA4erfUYnHbgU4UB6DlBOrsdEFRtcc8sCkdQ= @@ -30,10 +32,6 @@ github.com/hajimehoshi/oto v0.6.1 h1:7cJz/zRQV4aJvMSSRqzN2TImoVVMpE0BCY4nrNJaDOM github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= github.com/hajimehoshi/oto v0.7.1 h1:I7maFPz5MBCwiutOrz++DLdbr4rTzBsbBuV2VpgU9kk= github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos= -github.com/issadarkthing/anko v1.0.0 h1:CP5SI/C51kWQvufFEzJL9udui0fxt42yffaipG0mg/A= -github.com/issadarkthing/anko v1.0.0/go.mod h1:UE75FVFortpN/0PUWsp+kekz2MsIeMrhg769xLhi880= -github.com/issadarkthing/anko v1.0.1 h1:F0y32Dtlh0KaNgkJ0lIlOkIQysOsSdJnPPN7HhiI2Do= -github.com/issadarkthing/anko v1.0.1/go.mod h1:UE75FVFortpN/0PUWsp+kekz2MsIeMrhg769xLhi880= github.com/jfreymuth/oggvorbis v1.0.0/go.mod h1:abe6F9QRjuU9l+2jek3gj46lu40N4qlYxh2grqkLEDM= github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= @@ -71,13 +69,11 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tj/go-spin v1.1.0 h1:lhdWZsvImxvZ3q1C5OIB7d72DuOwP4O2NdBg9PyzNds= github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/ztrue/tracerr v0.3.0 h1:lDi6EgEYhPYPnKcjsYzmWw4EkFEoA/gfe+I9Y5f+h6Y= github.com/ztrue/tracerr v0.3.0/go.mod h1:qEalzze4VN9O8tnhBXScfCrmoJo10o8TN5ciKjm6Mww= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20180710024300-14dda7b62fcd/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= @@ -97,14 +93,11 @@ golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f/go.mod h1:skQtrUTUwhdJvXM golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -112,13 +105,10 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78 h1:nVuTkr9L6Bq62qpUqKo/RnZCFfzDBL0bYo6w9OJUqZY= golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -127,11 +117,10 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa h1:5E4dL8+NgFOgjwbTKz+OOEGGhP+ectTmF842l6KjupQ= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/playlist.go b/playlist.go index 9b87ab9..f8c582f 100644 --- a/playlist.go +++ b/playlist.go @@ -64,6 +64,7 @@ func (p *Playlist) help() []string { "p paste file", "/ find in playlist", "s search audio from youtube", + "t edit mp3 tags", } } @@ -170,6 +171,7 @@ func newPlaylist(args Args) *Playlist { 'y': "yank", 'p': "paste", '/': "playlist_search", + 't': "edit_tags", } for key, cmd := range cmds { @@ -549,12 +551,18 @@ func ytdl(url string, selPlaylist *tview.TreeNode) error { "%s/%%(title)s.%%(ext)s", dir) + metaData := fmt.Sprintf("%%(artist)s - %%(title)s") + args := []string{ "--extract-audio", "--audio-format", "mp3", "--output", outputDir, + "--add-metadata", + "--embed-thumbnail", + "--metadata-from-title", + metaData, // "--cookies", // "~/Downloads/youtube.com_cookies.txt", url, @@ -600,6 +608,65 @@ func ytdl(url string, selPlaylist *tview.TreeNode) error { return nil } +// Download audio subtitle from youtube audio +func ytdlSubtitle(url string, selPlaylist *tview.TreeNode) error { + + // lookup if youtube-dl exists + _, err := exec.LookPath("youtube-dl") + + if err != nil { + defaultTimedPopup(" Error ", "youtube-dl is not in your $PATH") + + return tracerr.Wrap(err) + } + + selAudioFile := selPlaylist.GetReference().(*AudioFile) + dir := selAudioFile.path + + // defaultTimedPopup(" Ytdl ", "Downloading subtitles") + + // specify the output path for ytdl + outputDir := fmt.Sprintf( + "%s/%%(title)s.%%(ext)s", + dir) + + langSubtitle := "en,zh-Hans" + + args := []string{ + "--skip-download", + "--output", + outputDir, + "--write-sub", + "--sub-lang", + langSubtitle, + "--convert-subs", + "lrc", + url, + } + + cmd := exec.Command("youtube-dl", args...) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + // blocking + err = cmd.Run() + + if err != nil { + defaultTimedPopup(" Error ", "Error running youtube-dl") + return tracerr.Wrap(err) + } + + playlistPath := dir + audioPath := extractFilePath(stdout.Bytes(), playlistPath) + + downloadFinishedMessage := fmt.Sprintf("Finished downloading subtitles\n%s", getName(audioPath)) + defaultTimedPopup(" Ytdl ", downloadFinishedMessage) + gomu.app.Draw() + + return nil +} + // Add songs and their directories in Playlist panel func populate(root *tview.TreeNode, rootPath string) error { diff --git a/popup.go b/popup.go index 82185b5..ab987f2 100644 --- a/popup.go +++ b/popup.go @@ -4,11 +4,14 @@ package main import ( "fmt" + "io/ioutil" + "path/filepath" "regexp" "strings" "time" "unicode/utf8" + "github.com/bogem/id3v2" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" "github.com/sahilm/fuzzy" @@ -297,6 +300,11 @@ func downloadMusicPopup(selPlaylist *tview.TreeNode) { logError(err) } }() + go func() { + if err := ytdlSubtitle(url, selPlaylist); err != nil { + logError(err) + } + }() } else { defaultTimedPopup("Invalid url", "Invalid youtube url was given") } @@ -662,3 +670,83 @@ func replPopup() { gomu.pages.AddPage(popupId, center(flex, 90, 30), true, true) gomu.popups.push(flex) } + +func tagPopup(node *AudioFile) bool { + var tag *id3v2.Tag + var err error + if node.isAudioFile { + tag, err = id3v2.Open(node.path, id3v2.Options{Parse: true}) + if err != nil { + logError(err) + return false + } + defer tag.Close() + } else { + return false + } + + popupID := "tag-input-popup" + + var lyricsAvailable []string + pathToFile, _ := filepath.Split(node.path) + + files, err := ioutil.ReadDir(pathToFile) + + if err != nil { + logError(err) + return false + } + + for _, file := range files { + + if filepath.Ext(file.Name()) == ".lrc" { + lyricsAvailable = append(lyricsAvailable, file.Name()) + } + } + form := tview.NewForm(). + AddInputField("Artist", tag.Artist(), 20, nil, nil). + AddInputField("Title", tag.Title(), 20, nil, nil). + AddInputField("Album", tag.Album(), 20, nil, nil). + AddCheckbox("Embed Lyrics", false, nil). + AddDropDown("Lyrics Available", lyricsAvailable, 0, nil) + + form.SetBackgroundColor(gomu.colors.popup). + SetBackgroundColor(gomu.colors.popup). + SetTitle(node.name). + SetTitleAlign(tview.AlignLeft). + SetBorder(true). + SetBorderPadding(1, 0, 2, 2) + + gomu.pages. + AddPage(popupID, center(form, 60, 30), true, true) + gomu.popups.push(form) + + form.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey { + switch e.Key() { + case tcell.KeyEnter: + tag, err = id3v2.Open(node.path, id3v2.Options{Parse: true}) + if err != nil { + logError(err) + } + defer tag.Close() + tag.SetArtist(form.GetFormItemByLabel("Artist").(*tview.InputField).GetText()) + tag.SetTitle(form.GetFormItemByLabel("Title").(*tview.InputField).GetText()) + tag.SetAlbum(form.GetFormItemByLabel("Album").(*tview.InputField).GetText()) + // Write tag to mp3. + if err := tag.Save(); err != nil { + defaultTimedPopup(" Error ", err.Error()) + logError(err) + } + defaultTimedPopup(" Success ", "Tag update successfully") + gomu.pages.RemovePage(popupID) + gomu.popups.pop() + + case tcell.KeyEsc: + gomu.pages.RemovePage(popupID) + gomu.popups.pop() + } + + return e + }) + return true +}