mirror of
https://github.com/issadarkthing/gomu.git
synced 2025-04-25 13:48:49 +08:00
Merge pull request #51 from tramhao/master
Fix race conditions. Add thumbnail preview. Fix search bar freezes whole UI
This commit is contained in:
commit
3c95c94980
@ -133,4 +133,7 @@ I just wanted to implement my own music player with a programming language i'm c
|
||||
- pause
|
||||
- forward and rewind
|
||||
|
||||
### Album Photo
|
||||
For songs downloaded by Gomu, the thumbnail will be embeded as Album cover. If you're not satisfied with the cover, you can edit it with kid3 and attach an image as album cover. Jpeg is tested, but other formats should work as well.
|
||||
|
||||
Seeking and more advanced stuff has not yet been implemented; feel free to contribute :)
|
||||
|
77
command.go
77
command.go
@ -62,10 +62,8 @@ func (c Command) defineCommands() {
|
||||
return
|
||||
}
|
||||
|
||||
err := gomu.playlist.deleteSong(audioFile)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
}
|
||||
gomu.playlist.deleteSong(audioFile)
|
||||
|
||||
})
|
||||
|
||||
c.define("youtube_search", func() {
|
||||
@ -94,7 +92,13 @@ func (c Command) defineCommands() {
|
||||
audioFile := gomu.playlist.getCurrentFile()
|
||||
currNode := gomu.playlist.GetCurrentNode()
|
||||
if audioFile.isAudioFile {
|
||||
gomu.queue.enqueue(audioFile)
|
||||
gomu.queue.pushFront(audioFile)
|
||||
if len(gomu.queue.items) == 1 && !gomu.player.IsRunning() {
|
||||
err := gomu.queue.playQueue()
|
||||
if err != nil {
|
||||
errorPopup(err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
currNode.SetExpanded(true)
|
||||
}
|
||||
@ -121,6 +125,12 @@ func (c Command) defineCommands() {
|
||||
|
||||
if !bulkAdd {
|
||||
gomu.playlist.addAllToQueue(currNode)
|
||||
if len(gomu.queue.items) > 0 && !gomu.player.IsRunning() {
|
||||
err := gomu.queue.playQueue()
|
||||
if err != nil {
|
||||
errorPopup(err)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -130,6 +140,12 @@ func (c Command) defineCommands() {
|
||||
|
||||
if label == "yes" {
|
||||
gomu.playlist.addAllToQueue(currNode)
|
||||
if len(gomu.queue.items) > 0 && !gomu.player.IsRunning() {
|
||||
err := gomu.queue.playQueue()
|
||||
if err != nil {
|
||||
errorPopup(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
@ -205,7 +221,12 @@ func (c Command) defineCommands() {
|
||||
}
|
||||
|
||||
gomu.queue.pushFront(a)
|
||||
gomu.player.Skip()
|
||||
|
||||
if gomu.player.IsRunning() {
|
||||
gomu.player.Skip()
|
||||
} else {
|
||||
gomu.queue.playQueue()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -307,64 +328,64 @@ func (c Command) defineCommands() {
|
||||
|
||||
c.define("forward", func() {
|
||||
if gomu.player.IsRunning() && !gomu.player.IsPaused() {
|
||||
position := gomu.playingBar.progress + 10
|
||||
if position < gomu.playingBar.full {
|
||||
position := gomu.playingBar.getProgress() + 10
|
||||
if position < gomu.playingBar.getFull() {
|
||||
err := gomu.player.Seek(position)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
errorPopup(err)
|
||||
}
|
||||
gomu.playingBar.progress = position
|
||||
gomu.playingBar.setProgress(position)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
c.define("rewind", func() {
|
||||
if gomu.player.IsRunning() && !gomu.player.IsPaused() {
|
||||
position := gomu.playingBar.progress - 10
|
||||
position := gomu.playingBar.getProgress() - 10
|
||||
if position-1 > 0 {
|
||||
err := gomu.player.Seek(position)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
errorPopup(err)
|
||||
}
|
||||
gomu.playingBar.progress = position
|
||||
gomu.playingBar.setProgress(position)
|
||||
} else {
|
||||
err := gomu.player.Seek(0)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
errorPopup(err)
|
||||
}
|
||||
gomu.playingBar.progress = 0
|
||||
gomu.playingBar.setProgress(0)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
c.define("forward_fast", func() {
|
||||
if gomu.player.IsRunning() && !gomu.player.IsPaused() {
|
||||
position := gomu.playingBar.progress + 60
|
||||
if position < gomu.playingBar.full {
|
||||
position := gomu.playingBar.getProgress() + 60
|
||||
if position < gomu.playingBar.getFull() {
|
||||
err := gomu.player.Seek(position)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
errorPopup(err)
|
||||
}
|
||||
gomu.playingBar.progress = position
|
||||
gomu.playingBar.setProgress(position)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
c.define("rewind_fast", func() {
|
||||
if gomu.player.IsRunning() && !gomu.player.IsPaused() {
|
||||
position := gomu.playingBar.progress - 60
|
||||
position := gomu.playingBar.getProgress() - 60
|
||||
if position-1 > 0 {
|
||||
err := gomu.player.Seek(position)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
errorPopup(err)
|
||||
}
|
||||
gomu.playingBar.progress = position
|
||||
gomu.playingBar.setProgress(position)
|
||||
} else {
|
||||
err := gomu.player.Seek(0)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
errorPopup(err)
|
||||
}
|
||||
gomu.playingBar.progress = 0
|
||||
gomu.playingBar.setProgress(0)
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -372,14 +393,14 @@ func (c Command) defineCommands() {
|
||||
c.define("yank", func() {
|
||||
err := gomu.playlist.yank()
|
||||
if err != nil {
|
||||
logError(err)
|
||||
errorPopup(err)
|
||||
}
|
||||
})
|
||||
|
||||
c.define("paste", func() {
|
||||
err := gomu.playlist.paste()
|
||||
if err != nil {
|
||||
logError(err)
|
||||
errorPopup(err)
|
||||
}
|
||||
})
|
||||
|
||||
@ -410,7 +431,6 @@ func (c Command) defineCommands() {
|
||||
err := lyricPopup(lang, audioFile, &wg)
|
||||
if err != nil {
|
||||
errorPopup(err)
|
||||
gomu.app.Draw()
|
||||
}
|
||||
}()
|
||||
}
|
||||
@ -427,7 +447,6 @@ func (c Command) defineCommands() {
|
||||
err := lyricPopup(lang, audioFile, &wg)
|
||||
if err != nil {
|
||||
errorPopup(err)
|
||||
gomu.app.Draw()
|
||||
}
|
||||
}()
|
||||
}
|
||||
@ -437,7 +456,6 @@ func (c Command) defineCommands() {
|
||||
err := gomu.playingBar.delayLyric(500)
|
||||
if err != nil {
|
||||
errorPopup(err)
|
||||
gomu.app.Draw()
|
||||
}
|
||||
})
|
||||
|
||||
@ -445,7 +463,6 @@ func (c Command) defineCommands() {
|
||||
err := gomu.playingBar.delayLyric(-500)
|
||||
if err != nil {
|
||||
errorPopup(err)
|
||||
gomu.app.Draw()
|
||||
}
|
||||
})
|
||||
|
||||
|
4
go.mod
4
go.mod
@ -6,7 +6,9 @@ require (
|
||||
github.com/PuerkitoBio/goquery v1.6.1 // indirect
|
||||
github.com/antchfx/htmlquery v1.2.3 // indirect
|
||||
github.com/antchfx/xmlquery v1.3.4 // indirect
|
||||
github.com/bogem/id3v2 v1.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/faiface/beep v1.0.2
|
||||
github.com/gdamore/tcell/v2 v2.2.0
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
@ -26,6 +28,8 @@ require (
|
||||
github.com/tj/go-spin v1.1.0
|
||||
github.com/tramhao/id3v2 v1.2.1
|
||||
github.com/ztrue/tracerr v0.3.0
|
||||
gitlab.com/diamondburned/ueberzug-go v0.0.0-20190521043425-7c15a5f63b06
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
golang.org/x/exp v0.0.0-20201229011636-eab1b5eb1a03 // indirect
|
||||
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
|
||||
|
15
go.sum
15
go.sum
@ -1,5 +1,12 @@
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJXZHsaM8b6OLVo6muQUQd4CwkH/D3fnnbHXA=
|
||||
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ=
|
||||
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g=
|
||||
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/BurntSushi/xgbutil v0.0.0-20160919175755-f7c97cef3b4e h1:4ZrkT/RzpnROylmoQL57iVUL57wGKTR5O6KpVnbm2tA=
|
||||
github.com/BurntSushi/xgbutil v0.0.0-20160919175755-f7c97cef3b4e/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k=
|
||||
github.com/PuerkitoBio/goquery v1.6.1 h1:FgjbQZKl5HTmcn4sKBgvx8vv63nhyhIpv7lJpFGCWpk=
|
||||
github.com/PuerkitoBio/goquery v1.6.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
|
||||
@ -16,6 +23,8 @@ github.com/bogem/id3v2 v1.2.0/go.mod h1:t78PK5AQ56Q47kizpYiV6gtjj3jfxlz87oFpty8D
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/faiface/beep v1.0.2 h1:UB5DiRNmA4erfUYnHbgU4UB6DlBOrsdEFRtcc8sCkdQ=
|
||||
github.com/faiface/beep v1.0.2/go.mod h1:1yLb5yRdHMsovYYWVqYLioXkVuziCSITW1oarTeduQM=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
@ -66,6 +75,8 @@ github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
|
||||
github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mewkiz/flac v1.0.5/go.mod h1:EHZNU32dMF6alpurYyKHDLYpW1lYpBZ5WrXi/VuNIGs=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@ -92,9 +103,12 @@ github.com/tramhao/id3v2 v1.2.1 h1:h8tXj1ReHoGwIIZEDp+fiDhGhf2wpCyrxEKLKVmhQw8=
|
||||
github.com/tramhao/id3v2 v1.2.1/go.mod h1:4jmC9bwoDhtGTsDkEBwSUlUgJq/D+8w4626jvM1Oo1k=
|
||||
github.com/ztrue/tracerr v0.3.0 h1:lDi6EgEYhPYPnKcjsYzmWw4EkFEoA/gfe+I9Y5f+h6Y=
|
||||
github.com/ztrue/tracerr v0.3.0/go.mod h1:qEalzze4VN9O8tnhBXScfCrmoJo10o8TN5ciKjm6Mww=
|
||||
gitlab.com/diamondburned/ueberzug-go v0.0.0-20190521043425-7c15a5f63b06 h1:lGu8YGHgq9ABb00JDQewrqhKIvku+/1uFsnq/QUeiU8=
|
||||
gitlab.com/diamondburned/ueberzug-go v0.0.0-20190521043425-7c15a5f63b06/go.mod h1:UKSsoWKXcGQXxWYkXFB4z/UqJtIF3pZT6fHm6beLPKM=
|
||||
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 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
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=
|
||||
@ -104,6 +118,7 @@ golang.org/x/exp v0.0.0-20201229011636-eab1b5eb1a03/go.mod h1:I6l2HNBLBZEcrOoCpy
|
||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI=
|
||||
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/mobile v0.0.0-20180806140643-507816974b79/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
|
2
gomu.go
2
gomu.go
@ -140,7 +140,7 @@ func (g *Gomu) setUnfocusPanel(panel Panel) {
|
||||
func (g *Gomu) quit(args Args) error {
|
||||
|
||||
if !*args.empty {
|
||||
err := gomu.queue.saveQueue(true)
|
||||
err := gomu.queue.saveQueue()
|
||||
if err != nil {
|
||||
return tracerr.Wrap(err)
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
// Package hook is handling event hookds
|
||||
package hook
|
||||
|
||||
type EventHook struct {
|
||||
events map[string][]func()
|
||||
}
|
||||
|
||||
// NewNewEventHook returns new instance of EventHook
|
||||
// NewEventHook returns new instance of EventHook
|
||||
func NewEventHook() *EventHook {
|
||||
return &EventHook{make(map[string][]func())}
|
||||
}
|
||||
|
32
lyric/lrc.go
32
lyric/lrc.go
@ -16,6 +16,7 @@
|
||||
package lyric
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"runtime"
|
||||
@ -127,11 +128,11 @@ func (lyric *Lyric) NewFromLRC(s string) (err error) {
|
||||
for _, v := range lyric.UnsyncedCaptions {
|
||||
var s id3v2.SyncedText
|
||||
s.Text = v.Text
|
||||
if lyric.Offset >= 0 {
|
||||
s.Timestamp = v.Timestamp + uint32(lyric.Offset)
|
||||
if lyric.Offset <= 0 {
|
||||
s.Timestamp = v.Timestamp + uint32(-lyric.Offset)
|
||||
} else {
|
||||
if v.Timestamp > uint32(-lyric.Offset) {
|
||||
s.Timestamp = v.Timestamp - uint32(-lyric.Offset)
|
||||
if v.Timestamp > uint32(lyric.Offset) {
|
||||
s.Timestamp = v.Timestamp - uint32(lyric.Offset)
|
||||
} else {
|
||||
s.Timestamp = 0
|
||||
}
|
||||
@ -265,3 +266,26 @@ func timeLRC(t uint32) string {
|
||||
res := fmt.Sprintf("%02d:%02d.%03d", m, s, ms)
|
||||
return res
|
||||
}
|
||||
|
||||
// GetText will fetch lyric by time in seconds
|
||||
func (lyric *Lyric) GetText(time int) (string, error) {
|
||||
|
||||
if len(lyric.SyncedCaptions) == 0 {
|
||||
return "", errors.New("no synced lyric found")
|
||||
}
|
||||
|
||||
// here we want to show lyric 1 second earlier
|
||||
time = time*1000 + 1000
|
||||
|
||||
text := lyric.SyncedCaptions[0].Text
|
||||
|
||||
for _, v := range lyric.SyncedCaptions {
|
||||
if time >= int(v.Timestamp) {
|
||||
text = v.Text
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return text, nil
|
||||
}
|
||||
|
@ -19,9 +19,10 @@ type SongTag struct {
|
||||
LyricID string
|
||||
}
|
||||
|
||||
type GetLyrics interface {
|
||||
GetLyric(songTag *SongTag) (string, error)
|
||||
GetLyricOptions(search string) ([]*SongTag, error)
|
||||
// LyricFetcher is the interface to get lyrics via different language
|
||||
type LyricFetcher interface {
|
||||
LyricFetch(songTag *SongTag) (string, error)
|
||||
LyricOptions(search string) ([]*SongTag, error)
|
||||
}
|
||||
|
||||
// cleanHTML parses html text to valid utf-8 text
|
||||
|
@ -42,10 +42,10 @@ type tagLyric struct {
|
||||
Tlyric string `json:"tlyric"`
|
||||
}
|
||||
|
||||
type GetLyricCn struct{}
|
||||
type LyricFetcherCn struct{}
|
||||
|
||||
// GetLyricOptions queries available song lyrics. It returns slice of SongTag
|
||||
func (cn GetLyricCn) GetLyricOptions(search string) ([]*SongTag, error) {
|
||||
// LyricOptions queries available song lyrics. It returns slice of SongTag
|
||||
func (cn LyricFetcherCn) LyricOptions(search string) ([]*SongTag, error) {
|
||||
|
||||
serviceProvider := "netease"
|
||||
results, err := getLyricOptionsCnByProvider(search, serviceProvider)
|
||||
@ -63,9 +63,9 @@ func (cn GetLyricCn) GetLyricOptions(search string) ([]*SongTag, error) {
|
||||
return results, err
|
||||
}
|
||||
|
||||
// GetLyric should receive songTag that was returned from getLyricOptions
|
||||
// LyricFetch should receive songTag that was returned from getLyricOptions
|
||||
// and returns lyric of the queried song.
|
||||
func (cn GetLyricCn) GetLyric(songTag *SongTag) (lyricString string, err error) {
|
||||
func (cn LyricFetcherCn) LyricFetch(songTag *SongTag) (lyricString string, err error) {
|
||||
|
||||
urlSearch := "http://api.sunyj.xyz"
|
||||
|
||||
|
@ -8,11 +8,11 @@ import (
|
||||
"github.com/gocolly/colly"
|
||||
)
|
||||
|
||||
type GetLyricEn struct{}
|
||||
type LyricFetcherEn struct{}
|
||||
|
||||
// GetLyric should receive SongTag that was returned from GetLyricOptions, and
|
||||
// LyricFetch should receive SongTag that was returned from GetLyricOptions, and
|
||||
// returns lyric of the queried song.
|
||||
func (en GetLyricEn) GetLyric(songTag *SongTag) (string, error) {
|
||||
func (en LyricFetcherEn) LyricFetch(songTag *SongTag) (string, error) {
|
||||
|
||||
var lyric string
|
||||
c := colly.NewCollector()
|
||||
@ -39,8 +39,8 @@ func (en GetLyricEn) GetLyric(songTag *SongTag) (string, error) {
|
||||
return "", fmt.Errorf("lyric not compatible")
|
||||
}
|
||||
|
||||
// GetLyricOptions queries available song lyrics. It returns slice of SongTag
|
||||
func (en GetLyricEn) GetLyricOptions(search string) ([]*SongTag, error) {
|
||||
// LyricOptions queries available song lyrics. It returns slice of SongTag
|
||||
func (en LyricFetcherEn) LyricOptions(search string) ([]*SongTag, error) {
|
||||
|
||||
var songTags []*SongTag
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
// Package player is the place actually play the music
|
||||
package player
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/faiface/beep"
|
||||
@ -31,6 +33,7 @@ type Player struct {
|
||||
songFinish func(Audio)
|
||||
songStart func(Audio)
|
||||
songSkip func(Audio)
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// New returns new Player instance.
|
||||
@ -100,15 +103,16 @@ func (p *Player) Run(currSong Audio) error {
|
||||
}
|
||||
|
||||
p.streamSeekCloser = stream
|
||||
p.format = &format
|
||||
|
||||
// song duration
|
||||
p.length = p.format.SampleRate.D(p.streamSeekCloser.Len())
|
||||
p.length = format.SampleRate.D(p.streamSeekCloser.Len())
|
||||
|
||||
sr := beep.SampleRate(48000)
|
||||
if !p.hasInit {
|
||||
|
||||
// p.mu.Lock()
|
||||
err := speaker.Init(sr, sr.N(time.Second/10))
|
||||
// p.mu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
return tracerr.Wrap(err)
|
||||
@ -120,7 +124,7 @@ func (p *Player) Run(currSong Audio) error {
|
||||
p.currentSong = currSong
|
||||
|
||||
// resample to adapt to sample rate of new songs
|
||||
resampled := beep.Resample(4, p.format.SampleRate, sr, p.streamSeekCloser)
|
||||
resampled := beep.Resample(4, format.SampleRate, sr, p.streamSeekCloser)
|
||||
|
||||
sstreamer := beep.Seq(resampled, beep.Callback(func() {
|
||||
p.isRunning = false
|
||||
@ -134,7 +138,10 @@ func (p *Player) Run(currSong Audio) error {
|
||||
Paused: false,
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
p.format = &format
|
||||
p.ctrl = ctrl
|
||||
p.mu.Unlock()
|
||||
resampler := beep.ResampleRatio(4, 1, ctrl)
|
||||
|
||||
volume := &effects.Volume{
|
||||
@ -170,7 +177,7 @@ func (p *Player) Play() {
|
||||
speaker.Unlock()
|
||||
}
|
||||
|
||||
// Volume up and volume down using -0.5 or +0.5.
|
||||
// SetVolume set volume up and volume down using -0.5 or +0.5.
|
||||
func (p *Player) SetVolume(v float64) float64 {
|
||||
|
||||
// check if no songs playing currently
|
||||
@ -186,7 +193,7 @@ func (p *Player) SetVolume(v float64) float64 {
|
||||
return p.volume
|
||||
}
|
||||
|
||||
// Toggles the pause state.
|
||||
// TogglePause toggles the pause state.
|
||||
func (p *Player) TogglePause() {
|
||||
|
||||
if p.ctrl == nil {
|
||||
@ -200,7 +207,7 @@ func (p *Player) TogglePause() {
|
||||
}
|
||||
}
|
||||
|
||||
// Skips current song.
|
||||
// Skip current song.
|
||||
func (p *Player) Skip() {
|
||||
|
||||
p.execSongSkip(p.currentSong)
|
||||
@ -210,17 +217,23 @@ func (p *Player) Skip() {
|
||||
}
|
||||
|
||||
// drain the stream
|
||||
speaker.Lock()
|
||||
p.ctrl.Streamer = nil
|
||||
|
||||
p.streamSeekCloser.Close()
|
||||
p.isRunning = false
|
||||
p.format = nil
|
||||
speaker.Unlock()
|
||||
|
||||
p.execSongFinish(p.currentSong)
|
||||
}
|
||||
|
||||
// GetPosition returns the current position of audio file.
|
||||
func (p *Player) GetPosition() time.Duration {
|
||||
|
||||
p.mu.Lock()
|
||||
speaker.Lock()
|
||||
defer speaker.Unlock()
|
||||
defer p.mu.Unlock()
|
||||
if p.format == nil || p.streamSeekCloser == nil {
|
||||
return 1
|
||||
}
|
||||
@ -228,16 +241,22 @@ func (p *Player) GetPosition() time.Duration {
|
||||
return p.format.SampleRate.D(p.streamSeekCloser.Position())
|
||||
}
|
||||
|
||||
// seek is the function to move forward and rewind
|
||||
// Seek is the function to move forward and rewind
|
||||
func (p *Player) Seek(pos int) error {
|
||||
p.mu.Lock()
|
||||
speaker.Lock()
|
||||
defer speaker.Unlock()
|
||||
defer p.mu.Unlock()
|
||||
err := p.streamSeekCloser.Seek(pos * int(p.format.SampleRate))
|
||||
return err
|
||||
}
|
||||
|
||||
// IsPaused is used to distinguish the player between pause and stop
|
||||
func (p *Player) IsPaused() bool {
|
||||
p.mu.Lock()
|
||||
speaker.Lock()
|
||||
defer speaker.Unlock()
|
||||
defer p.mu.Unlock()
|
||||
if p.ctrl == nil {
|
||||
return false
|
||||
}
|
||||
@ -266,7 +285,7 @@ func (p *Player) IsRunning() bool {
|
||||
return p.isRunning
|
||||
}
|
||||
|
||||
// Gets the length of the song in the queue
|
||||
// GetLength return the length of the song in the queue
|
||||
func GetLength(audioPath string) (time.Duration, error) {
|
||||
f, err := os.Open(audioPath)
|
||||
|
||||
|
147
playingbar.go
147
playingbar.go
@ -3,15 +3,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/tramhao/id3v2"
|
||||
"github.com/ztrue/tracerr"
|
||||
ugo "gitlab.com/diamondburned/ueberzug-go"
|
||||
|
||||
"github.com/issadarkthing/gomu/lyric"
|
||||
)
|
||||
@ -19,15 +25,16 @@ import (
|
||||
// PlayingBar shows song name, progress and lyric
|
||||
type PlayingBar struct {
|
||||
*tview.Frame
|
||||
full int
|
||||
update chan struct{}
|
||||
progress int
|
||||
skip bool
|
||||
text *tview.TextView
|
||||
hasTag bool
|
||||
tag *id3v2.Tag
|
||||
subtitle *lyric.Lyric
|
||||
subtitles []*lyric.Lyric
|
||||
full int64
|
||||
update chan struct{}
|
||||
progress int64
|
||||
skip bool
|
||||
text *tview.TextView
|
||||
hasTag bool
|
||||
tag *id3v2.Tag
|
||||
subtitle *lyric.Lyric
|
||||
subtitles []*lyric.Lyric
|
||||
albumPhoto *ugo.Image
|
||||
}
|
||||
|
||||
func (p *PlayingBar) help() []string {
|
||||
@ -60,9 +67,12 @@ func (p *PlayingBar) run() error {
|
||||
for {
|
||||
|
||||
// stop progressing if song ends or skipped
|
||||
if p.progress > p.full || p.skip {
|
||||
progress := p.getProgress()
|
||||
full := p.getFull()
|
||||
|
||||
if progress > full || p.skip {
|
||||
p.skip = false
|
||||
p.progress = 0
|
||||
p.setProgress(0)
|
||||
break
|
||||
}
|
||||
|
||||
@ -71,42 +81,31 @@ func (p *PlayingBar) run() error {
|
||||
continue
|
||||
}
|
||||
|
||||
p.progress = int(gomu.player.GetPosition().Seconds())
|
||||
// p.progress = int(gomu.player.GetPosition().Seconds())
|
||||
p.setProgress(int(gomu.player.GetPosition().Seconds()))
|
||||
|
||||
start, err := time.ParseDuration(strconv.Itoa(p.progress) + "s")
|
||||
start, err := time.ParseDuration(strconv.Itoa(progress) + "s")
|
||||
if err != nil {
|
||||
return tracerr.Wrap(err)
|
||||
}
|
||||
|
||||
end, err := time.ParseDuration(strconv.Itoa(p.full) + "s")
|
||||
end, err := time.ParseDuration(strconv.Itoa(full) + "s")
|
||||
|
||||
if err != nil {
|
||||
return tracerr.Wrap(err)
|
||||
}
|
||||
var width int
|
||||
gomu.app.QueueUpdate(func() {
|
||||
_, _, width, _ = p.GetInnerRect()
|
||||
})
|
||||
|
||||
_, _, width, _ := p.GetInnerRect()
|
||||
progressBar := progresStr(p.progress, p.full, width/2, "█", "━")
|
||||
progressBar := progresStr(progress, full, width/2, "█", "━")
|
||||
// our progress bar
|
||||
var lyricText string
|
||||
if p.subtitle != nil {
|
||||
for i := range p.subtitle.SyncedCaptions {
|
||||
startTime := int32(p.subtitle.SyncedCaptions[i].Timestamp)
|
||||
var endTime int32
|
||||
if i < len(p.subtitle.SyncedCaptions)-1 {
|
||||
endTime = int32(p.subtitle.SyncedCaptions[i+1].Timestamp)
|
||||
} else {
|
||||
// Here we display the last lyric until the end of song
|
||||
endTime = int32(p.full * 1000)
|
||||
}
|
||||
|
||||
// here the currentTime is delayed 1 second because we want to show lyrics earlier
|
||||
currentTime := int32(p.progress*1000) + 1000
|
||||
if currentTime >= startTime && currentTime <= endTime {
|
||||
lyricText = p.subtitle.SyncedCaptions[i].Text
|
||||
break
|
||||
} else {
|
||||
lyricText = ""
|
||||
}
|
||||
lyricText, err = p.subtitle.GetText(progress)
|
||||
if err != nil {
|
||||
return tracerr.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,17 +130,22 @@ func (p *PlayingBar) setSongTitle(title string) {
|
||||
p.Clear()
|
||||
titleColor := gomu.colors.title
|
||||
p.AddText(title, true, tview.AlignCenter, titleColor)
|
||||
|
||||
}
|
||||
|
||||
// Resets progress bar, ready for execution
|
||||
func (p *PlayingBar) newProgress(currentSong *AudioFile, full int) {
|
||||
p.full = full
|
||||
p.progress = 0
|
||||
p.setSongTitle(currentSong.name)
|
||||
p.setFull(full)
|
||||
p.setProgress(0)
|
||||
p.hasTag = false
|
||||
p.tag = nil
|
||||
p.subtitles = nil
|
||||
p.subtitle = nil
|
||||
if p.albumPhoto != nil {
|
||||
p.albumPhoto.Clear()
|
||||
p.albumPhoto.Destroy()
|
||||
p.albumPhoto = nil
|
||||
}
|
||||
|
||||
err := p.loadLyrics(currentSong.path)
|
||||
if err != nil {
|
||||
@ -176,6 +180,8 @@ func (p *PlayingBar) newProgress(currentSong *AudioFile, full int) {
|
||||
p.subtitle = p.subtitles[0]
|
||||
}
|
||||
}
|
||||
p.setSongTitle(currentSong.name)
|
||||
|
||||
}
|
||||
|
||||
// Sets default title and progress bar
|
||||
@ -186,6 +192,9 @@ func (p *PlayingBar) setDefault() {
|
||||
"%s ┣%s┫ %s", "00:00", strings.Repeat("━", width/2), "00:00",
|
||||
)
|
||||
p.text.SetText(text)
|
||||
if p.albumPhoto != nil {
|
||||
p.albumPhoto.Clear()
|
||||
}
|
||||
}
|
||||
|
||||
// Skips the current playing song
|
||||
@ -210,8 +219,8 @@ func (p *PlayingBar) switchLyrics() {
|
||||
|
||||
// only 1 subtitle, prompt to the user and select this one
|
||||
if len(p.subtitles) == 1 {
|
||||
defaultTimedPopup(" Warning ", p.subtitle.LangExt+" lyric is the only lyric available")
|
||||
p.subtitle = p.subtitles[0]
|
||||
defaultTimedPopup(" Warning ", p.subtitle.LangExt+" lyric is the only lyric available")
|
||||
return
|
||||
}
|
||||
|
||||
@ -236,7 +245,7 @@ func (p *PlayingBar) switchLyrics() {
|
||||
func (p *PlayingBar) delayLyric(lyricDelay int) (err error) {
|
||||
|
||||
if p.subtitle != nil {
|
||||
p.subtitle.Offset += int32(lyricDelay)
|
||||
p.subtitle.Offset -= int32(lyricDelay)
|
||||
err = embedLyric(gomu.player.GetCurrentSong().Path(), p.subtitle, false)
|
||||
if err != nil {
|
||||
return tracerr.Wrap(err)
|
||||
@ -272,6 +281,12 @@ func (p *PlayingBar) loadLyrics(currentSongPath string) error {
|
||||
p.hasTag = true
|
||||
p.tag = tag
|
||||
|
||||
if p.albumPhoto != nil {
|
||||
p.albumPhoto.Clear()
|
||||
p.albumPhoto.Destroy()
|
||||
p.albumPhoto = nil
|
||||
}
|
||||
|
||||
syltFrames := tag.GetFrames(tag.CommonID("Synchronised lyrics/text"))
|
||||
usltFrames := tag.GetFrames(tag.CommonID("Unsynchronised lyrics/text transcription"))
|
||||
|
||||
@ -298,5 +313,59 @@ func (p *PlayingBar) loadLyrics(currentSongPath string) error {
|
||||
}
|
||||
}
|
||||
|
||||
pictures := tag.GetFrames(tag.CommonID("Attached picture"))
|
||||
for _, f := range pictures {
|
||||
pic, ok := f.(id3v2.PictureFrame)
|
||||
if !ok {
|
||||
return errors.New("picture frame error")
|
||||
}
|
||||
|
||||
// Do something with picture frame.
|
||||
img1, err := imaging.Decode(bytes.NewReader(pic.Picture))
|
||||
if err != nil {
|
||||
return tracerr.Wrap(err)
|
||||
}
|
||||
dstImage128 := imaging.Fit(img1, 128, 128, imaging.Lanczos)
|
||||
|
||||
go gomu.app.QueueUpdateDraw(func() {
|
||||
x, y, _, _ := p.GetInnerRect()
|
||||
width, height, windowWidth, windowHeight := getConsoleSize()
|
||||
|
||||
p.albumPhoto, err = ugo.NewImage(dstImage128, (x+3)*windowWidth/width, (y+2)*windowHeight/height)
|
||||
if err != nil {
|
||||
errorPopup(err)
|
||||
}
|
||||
p.albumPhoto.Show()
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PlayingBar) getProgress() int {
|
||||
return int(atomic.LoadInt64(&p.progress))
|
||||
}
|
||||
|
||||
func (p *PlayingBar) setProgress(progress int) {
|
||||
atomic.StoreInt64(&p.progress, int64(progress))
|
||||
}
|
||||
|
||||
func (p *PlayingBar) getFull() int {
|
||||
return int(atomic.LoadInt64(&p.full))
|
||||
}
|
||||
|
||||
func (p *PlayingBar) setFull(full int) {
|
||||
atomic.StoreInt64(&p.full, int64(full))
|
||||
}
|
||||
|
||||
func getConsoleSize() (int, int, int, int) {
|
||||
var sz struct {
|
||||
rows uint16
|
||||
cols uint16
|
||||
xpixels uint16
|
||||
ypixels uint16
|
||||
}
|
||||
_, _, _ = syscall.Syscall(syscall.SYS_IOCTL,
|
||||
uintptr(syscall.Stdout), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&sz)))
|
||||
return int(sz.cols), int(sz.rows), int(sz.xpixels), int(sz.ypixels)
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ func Test_NewProgress(t *testing.T) {
|
||||
|
||||
p.newProgress(&audio, full)
|
||||
|
||||
if p.full != full {
|
||||
if p.full != int64(full) {
|
||||
t.Errorf("Expected %d; got %d", full, p.full)
|
||||
}
|
||||
|
||||
|
217
playlist.go
217
playlist.go
@ -4,6 +4,7 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
@ -82,12 +83,8 @@ type Playlist struct {
|
||||
// number of downloads
|
||||
download int
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
var (
|
||||
yankFile *AudioFile
|
||||
isYanked bool
|
||||
)
|
||||
}
|
||||
|
||||
func (p *Playlist) help() []string {
|
||||
|
||||
@ -247,7 +244,7 @@ func (p Playlist) getCurrentFile() *AudioFile {
|
||||
}
|
||||
|
||||
// Deletes song from filesystem
|
||||
func (p *Playlist) deleteSong(audioFile *AudioFile) (err error) {
|
||||
func (p *Playlist) deleteSong(audioFile *AudioFile) {
|
||||
|
||||
confirmationPopup(
|
||||
"Are you sure to delete this audio file?", func(_ int, buttonName string) {
|
||||
@ -256,52 +253,52 @@ func (p *Playlist) deleteSong(audioFile *AudioFile) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
audioName := getName(audioFile.path)
|
||||
// hehe we need to move focus to next node before delete it
|
||||
p.InputHandler()(tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone), nil)
|
||||
|
||||
err := os.Remove(audioFile.path)
|
||||
|
||||
if err != nil {
|
||||
|
||||
defaultTimedPopup(" Error ", "Unable to delete "+audioFile.name)
|
||||
|
||||
err = tracerr.Wrap(err)
|
||||
|
||||
} else {
|
||||
|
||||
defaultTimedPopup(" Success ",
|
||||
audioFile.name+"\nhas been deleted successfully")
|
||||
p.refresh()
|
||||
|
||||
// Here we remove the song from queue
|
||||
songPaths := gomu.queue.getItems()
|
||||
if audioName == getName(gomu.player.GetCurrentSong().Name()) {
|
||||
gomu.player.Skip()
|
||||
}
|
||||
for i, songPath := range songPaths {
|
||||
if strings.Contains(songPath, audioName) {
|
||||
gomu.queue.deleteItem(i)
|
||||
}
|
||||
}
|
||||
errorPopup(err)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Deletes playlist/dir from filesystem
|
||||
func (p *Playlist) deletePlaylist(audioFile *AudioFile) (err error) {
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
err = tracerr.Wrap(err)
|
||||
} else {
|
||||
defaultTimedPopup(
|
||||
" Success ",
|
||||
audioFile.name+"\nhas been deleted successfully")
|
||||
p.refresh()
|
||||
return tracerr.Wrap(err)
|
||||
}
|
||||
|
||||
return err
|
||||
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
|
||||
}
|
||||
|
||||
// Bulk add a playlist to queue
|
||||
@ -488,6 +485,7 @@ 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 {
|
||||
@ -500,11 +498,6 @@ func (p *Playlist) rename(newName string) error {
|
||||
return tracerr.Wrap(err)
|
||||
}
|
||||
|
||||
audio.path = newPath
|
||||
gomu.queue.saveQueue(false)
|
||||
gomu.queue.clearQueue()
|
||||
gomu.queue.loadQueue()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -737,6 +730,13 @@ func populate(root *tview.TreeNode, rootPath string, sortMtime bool) error {
|
||||
parent: root,
|
||||
}
|
||||
|
||||
audioLength, err := getTagLength(audioFile.path)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
}
|
||||
|
||||
audioFile.length = audioLength
|
||||
|
||||
displayText := setDisplayText(audioFile)
|
||||
|
||||
child.SetReference(audioFile)
|
||||
@ -771,60 +771,65 @@ func populate(root *tview.TreeNode, rootPath string, sortMtime bool) error {
|
||||
}
|
||||
|
||||
func (p *Playlist) yank() error {
|
||||
yankFile = p.getCurrentFile()
|
||||
if yankFile == nil {
|
||||
isYanked = false
|
||||
defaultTimedPopup(" Error! ", "No file has been yanked.")
|
||||
return nil
|
||||
p.yankFile = p.getCurrentFile()
|
||||
if p.yankFile == nil {
|
||||
return errors.New("no file has been yanked")
|
||||
}
|
||||
if yankFile.node == p.GetRoot() {
|
||||
isYanked = false
|
||||
defaultTimedPopup(" Error! ", "Please don't yank the root directory.")
|
||||
return nil
|
||||
if p.yankFile.node == p.GetRoot() {
|
||||
return errors.New("please don't yank the root directory")
|
||||
}
|
||||
isYanked = true
|
||||
defaultTimedPopup(" Success ", yankFile.name+"\n has been yanked successfully.")
|
||||
defaultTimedPopup(" Success ", p.yankFile.name+"\n has been yanked successfully.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Playlist) paste() error {
|
||||
if isYanked {
|
||||
isYanked = false
|
||||
oldPathDir, oldPathFileName := filepath.Split(yankFile.path)
|
||||
pasteFile := p.getCurrentFile()
|
||||
if pasteFile.isAudioFile {
|
||||
newPathDir, _ := filepath.Split(pasteFile.path)
|
||||
if oldPathDir == newPathDir {
|
||||
return nil
|
||||
}
|
||||
newPathFull := filepath.Join(newPathDir, oldPathFileName)
|
||||
err := os.Rename(yankFile.path, newPathFull)
|
||||
if err != nil {
|
||||
defaultTimedPopup(" Error ", yankFile.name+"\n has not been pasted.")
|
||||
return tracerr.Wrap(err)
|
||||
}
|
||||
defaultTimedPopup(" Success ", yankFile.name+"\n has been pasted to\n"+pasteFile.name)
|
||||
|
||||
} else {
|
||||
newPathDir := pasteFile.path
|
||||
if oldPathDir == newPathDir {
|
||||
return nil
|
||||
}
|
||||
newPathFull := filepath.Join(newPathDir, oldPathFileName)
|
||||
err := os.Rename(yankFile.path, newPathFull)
|
||||
if err != nil {
|
||||
defaultTimedPopup(" Error ", yankFile.name+"\n has not been pasted.")
|
||||
return tracerr.Wrap(err)
|
||||
}
|
||||
defaultTimedPopup(" Success ", yankFile.name+"\n has been pasted to\n"+pasteFile.name)
|
||||
|
||||
}
|
||||
|
||||
p.refresh()
|
||||
gomu.queue.updateQueueNames()
|
||||
if p.yankFile == nil {
|
||||
return errors.New("no file has been yanked")
|
||||
}
|
||||
|
||||
oldAudio := p.yankFile
|
||||
oldPathDir, oldPathFileName := filepath.Split(p.yankFile.path)
|
||||
pasteFile := p.getCurrentFile()
|
||||
var newPathDir string
|
||||
if pasteFile.isAudioFile {
|
||||
newPathDir, _ = filepath.Split(pasteFile.path)
|
||||
} else {
|
||||
newPathDir = pasteFile.path
|
||||
}
|
||||
|
||||
if oldPathDir == newPathDir {
|
||||
return nil
|
||||
}
|
||||
|
||||
newPathFull := filepath.Join(newPathDir, oldPathFileName)
|
||||
err := os.Rename(p.yankFile.path, newPathFull)
|
||||
if err != nil {
|
||||
return tracerr.Wrap(err)
|
||||
}
|
||||
|
||||
defaultTimedPopup(" Success ", p.yankFile.name+"\n has been pasted to\n"+newPathDir)
|
||||
|
||||
// keep queue references updated
|
||||
newAudio := oldAudio
|
||||
newAudio.path = 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
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -843,22 +848,34 @@ func setDisplayText(audioFile *AudioFile) string {
|
||||
return fmt.Sprintf(" %s %s", emojiDir, audioFile.name)
|
||||
}
|
||||
|
||||
// populateAudioLength is the most time consuming part of startup,
|
||||
// so here we initialize it separately
|
||||
func populateAudioLength(root *tview.TreeNode) error {
|
||||
root.Walk(func(node *tview.TreeNode, _ *tview.TreeNode) bool {
|
||||
audioFile := node.GetReference().(*AudioFile)
|
||||
if audioFile.isAudioFile {
|
||||
audioLength, err := getTagLength(audioFile.path)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return false
|
||||
}
|
||||
audioFile.length = audioLength
|
||||
// refreshByNode is called after rename of file or folder, to refresh queue info
|
||||
func (p *Playlist) refreshAfterRename(node *AudioFile, newName string) error {
|
||||
|
||||
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 {
|
||||
err := gomu.queue.renameItem(node, newNode)
|
||||
if err != nil {
|
||||
return tracerr.Wrap(err)
|
||||
}
|
||||
err = gomu.queue.updateCurrentSongName(node, newNode)
|
||||
if err != nil {
|
||||
return tracerr.Wrap(err)
|
||||
}
|
||||
} else {
|
||||
gomu.queue.updateQueuePath()
|
||||
err := gomu.queue.updateCurrentSongPath(node, newNode)
|
||||
if err != nil {
|
||||
return tracerr.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
gomu.queue.updateTitle()
|
||||
return nil
|
||||
}
|
||||
|
56
popup.go
56
popup.go
@ -67,6 +67,9 @@ func (s *Stack) pop() tview.Primitive {
|
||||
} else {
|
||||
// focus the panel if no popup left
|
||||
gomu.app.SetFocus(gomu.prevPanel.(tview.Primitive))
|
||||
if gomu.playingBar.albumPhoto != nil {
|
||||
gomu.playingBar.albumPhoto.Show()
|
||||
}
|
||||
}
|
||||
|
||||
return last
|
||||
@ -141,10 +144,13 @@ func confirmDeleteAllPopup(selPlaylist *tview.TreeNode) (err error) {
|
||||
gomu.popups.pop()
|
||||
|
||||
if confirmationText == "DELETE" {
|
||||
err = gomu.playlist.deletePlaylist(selPlaylist.GetReference().(*AudioFile))
|
||||
audioFile := selPlaylist.GetReference().(*AudioFile)
|
||||
err = gomu.playlist.deletePlaylist(audioFile)
|
||||
if err != nil {
|
||||
errorPopup(err)
|
||||
}
|
||||
gomu.queue.updateQueuePath()
|
||||
gomu.queue.updateCurrentSongDelete(audioFile)
|
||||
}
|
||||
|
||||
case tcell.KeyEscape:
|
||||
@ -320,6 +326,10 @@ func helpPopup(panel Panel) {
|
||||
return nil
|
||||
})
|
||||
|
||||
if gomu.playingBar.albumPhoto != nil {
|
||||
gomu.playingBar.albumPhoto.Clear()
|
||||
}
|
||||
|
||||
gomu.pages.AddPage("help-page", center(list, 50, 32), true, true)
|
||||
gomu.popups.push(list)
|
||||
}
|
||||
@ -436,7 +446,6 @@ func searchPopup(title string, stringsToMatch []string, handler func(selected st
|
||||
pattern := input.GetText()
|
||||
matches := fuzzy.Find(pattern, stringsToMatch)
|
||||
const highlight = "[red]%c[-]"
|
||||
// const highlight = "[red]%s[-]"
|
||||
|
||||
for _, match := range matches {
|
||||
var text strings.Builder
|
||||
@ -513,10 +522,12 @@ func searchPopup(title string, stringsToMatch []string, handler func(selected st
|
||||
// this is to fix the left border of search popup
|
||||
popupFrame := tview.NewFrame(popup)
|
||||
|
||||
if gomu.playingBar.albumPhoto != nil {
|
||||
gomu.playingBar.albumPhoto.Clear()
|
||||
}
|
||||
|
||||
gomu.pages.AddPage("search-input-popup", center(popupFrame, 70, 40), true, true)
|
||||
gomu.popups.push(popup)
|
||||
// This is to ensure the popup is shown even when paused
|
||||
gomu.app.Draw()
|
||||
}
|
||||
|
||||
// Creates new popup widget with default settings
|
||||
@ -558,24 +569,19 @@ func renamePopup(node *AudioFile) {
|
||||
}
|
||||
err := gomu.playlist.rename(newName)
|
||||
if err != nil {
|
||||
defaultTimedPopup(" Error ", err.Error())
|
||||
logError(err)
|
||||
errorPopup(err)
|
||||
}
|
||||
gomu.pages.RemovePage(popupID)
|
||||
gomu.popups.pop()
|
||||
gomu.playlist.refresh()
|
||||
|
||||
gomu.queue.updateQueueNames()
|
||||
gomu.setFocusPanel(gomu.playlist)
|
||||
gomu.prevPanel = gomu.playlist
|
||||
|
||||
root := gomu.playlist.GetRoot()
|
||||
root.Walk(func(node, _ *tview.TreeNode) bool {
|
||||
if strings.Contains(node.GetText(), newName) {
|
||||
gomu.playlist.setHighlight(node)
|
||||
}
|
||||
return true
|
||||
})
|
||||
err = gomu.playlist.refreshAfterRename(node, newName)
|
||||
if err != nil {
|
||||
errorPopup(err)
|
||||
}
|
||||
|
||||
case tcell.KeyEsc:
|
||||
gomu.pages.RemovePage(popupID)
|
||||
@ -728,6 +734,10 @@ func replPopup() {
|
||||
|
||||
flex.Box = flexBox
|
||||
|
||||
if gomu.playingBar.albumPhoto != nil {
|
||||
gomu.playingBar.albumPhoto.Clear()
|
||||
}
|
||||
|
||||
gomu.pages.AddPage(popupID, center(flex, 90, 30), true, true)
|
||||
gomu.popups.push(flex)
|
||||
}
|
||||
@ -854,10 +864,10 @@ func lyricPopup(lang string, audioFile *AudioFile, wg *sync.WaitGroup) error {
|
||||
|
||||
var titles []string
|
||||
|
||||
// below we chose languages with GetLyrics interfaces
|
||||
getLyric := lyricLang(lang)
|
||||
// below we chose LyricFetcher interfaces by language
|
||||
lyricFetcher := lyricLang(lang)
|
||||
|
||||
results, err := getLyric.GetLyricOptions(audioFile.name)
|
||||
results, err := lyricFetcher.LyricOptions(audioFile.name)
|
||||
if err != nil {
|
||||
return tracerr.Wrap(err)
|
||||
}
|
||||
@ -880,7 +890,7 @@ func lyricPopup(lang string, audioFile *AudioFile, wg *sync.WaitGroup) error {
|
||||
break
|
||||
}
|
||||
}
|
||||
lyricContent, err := getLyric.GetLyric(results[selectedIndex])
|
||||
lyricContent, err := lyricFetcher.LyricFetch(results[selectedIndex])
|
||||
if err != nil {
|
||||
errorPopup(err)
|
||||
gomu.app.Draw()
|
||||
@ -907,18 +917,20 @@ func lyricPopup(lang string, audioFile *AudioFile, wg *sync.WaitGroup) error {
|
||||
}()
|
||||
})
|
||||
|
||||
gomu.app.Draw()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func lyricLang(lang string) lyric.GetLyrics {
|
||||
func lyricLang(lang string) lyric.LyricFetcher {
|
||||
|
||||
switch lang {
|
||||
case "en":
|
||||
return lyric.GetLyricEn{}
|
||||
return lyric.LyricFetcherEn{}
|
||||
case "zh-CN":
|
||||
return lyric.GetLyricCn{}
|
||||
return lyric.LyricFetcherCn{}
|
||||
default:
|
||||
return lyric.GetLyricEn{}
|
||||
return lyric.LyricFetcherEn{}
|
||||
}
|
||||
|
||||
}
|
||||
|
217
queue.go
217
queue.go
@ -70,6 +70,10 @@ func (q *Queue) deleteItem(index int) (*AudioFile, error) {
|
||||
}
|
||||
|
||||
q.items = nItems
|
||||
// here we move to next item if not at the end
|
||||
if index < len(q.items) {
|
||||
q.next()
|
||||
}
|
||||
q.updateTitle()
|
||||
|
||||
}
|
||||
@ -155,18 +159,6 @@ func (q *Queue) dequeue() (*AudioFile, error) {
|
||||
// Add item to the list and returns the length of the queue
|
||||
func (q *Queue) enqueue(audioFile *AudioFile) (int, error) {
|
||||
|
||||
isTestEnv := os.Getenv("TEST") == "false"
|
||||
|
||||
if !gomu.player.IsRunning() && !gomu.player.IsPaused() && isTestEnv {
|
||||
|
||||
err := gomu.player.Run(audioFile)
|
||||
if err != nil {
|
||||
die(err)
|
||||
}
|
||||
|
||||
return q.GetItemCount(), nil
|
||||
}
|
||||
|
||||
if !audioFile.isAudioFile {
|
||||
return q.GetItemCount(), nil
|
||||
}
|
||||
@ -205,12 +197,12 @@ func (q *Queue) getItems() []string {
|
||||
}
|
||||
|
||||
// Save the current queue
|
||||
func (q *Queue) saveQueue(isQuit bool) error {
|
||||
func (q *Queue) saveQueue() error {
|
||||
|
||||
songPaths := q.getItems()
|
||||
var content strings.Builder
|
||||
|
||||
if gomu.player.HasInit() && isQuit && gomu.player.GetCurrentSong() != nil {
|
||||
if gomu.player.HasInit() && gomu.player.GetCurrentSong() != nil {
|
||||
currentSongPath := gomu.player.GetCurrentSong().Path()
|
||||
currentSongInQueue := false
|
||||
for _, songPath := range songPaths {
|
||||
@ -424,9 +416,198 @@ func sha1Hex(input string) string {
|
||||
}
|
||||
|
||||
// Modify the title of songs in queue
|
||||
func (q *Queue) updateQueueNames() error {
|
||||
q.saveQueue(false)
|
||||
q.clearQueue()
|
||||
q.loadQueue()
|
||||
func (q *Queue) renameItem(oldAudio *AudioFile, newAudio *AudioFile) error {
|
||||
for i, v := range q.items {
|
||||
if v.name != oldAudio.name {
|
||||
continue
|
||||
}
|
||||
err := q.insertItem(i, newAudio)
|
||||
if err != nil {
|
||||
return tracerr.Wrap(err)
|
||||
}
|
||||
_, err = q.deleteItem(i + 1)
|
||||
if err != nil {
|
||||
return tracerr.Wrap(err)
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// playQueue play the first item in the queue
|
||||
func (q *Queue) playQueue() error {
|
||||
|
||||
audioFile, err := q.dequeue()
|
||||
if err != nil {
|
||||
return tracerr.Wrap(err)
|
||||
}
|
||||
err = gomu.player.Run(audioFile)
|
||||
if err != nil {
|
||||
return tracerr.Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *Queue) insertItem(index int, audioFile *AudioFile) error {
|
||||
|
||||
if index > len(q.items)-1 {
|
||||
return tracerr.New("Index out of range")
|
||||
}
|
||||
|
||||
if index != -1 {
|
||||
songLength, err := getTagLength(audioFile.path)
|
||||
if err != nil {
|
||||
return tracerr.Wrap(err)
|
||||
}
|
||||
queueItemView := fmt.Sprintf(
|
||||
"[ %s ] %s", fmtDuration(songLength), getName(audioFile.name),
|
||||
)
|
||||
|
||||
q.InsertItem(index, queueItemView, audioFile.path, 0, nil)
|
||||
|
||||
var nItems []*AudioFile
|
||||
|
||||
for i, v := range q.items {
|
||||
|
||||
if i == index {
|
||||
nItems = append(nItems, audioFile)
|
||||
}
|
||||
|
||||
nItems = append(nItems, v)
|
||||
}
|
||||
|
||||
q.items = nItems
|
||||
q.updateTitle()
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//update the path information in queue
|
||||
func (q *Queue) updateQueuePath() {
|
||||
|
||||
var songs []string
|
||||
if len(q.items) < 1 {
|
||||
return
|
||||
}
|
||||
for _, v := range q.items {
|
||||
song := sha1Hex(getName(v.name))
|
||||
songs = append(songs, song)
|
||||
}
|
||||
|
||||
q.clearQueue()
|
||||
for _, v := range songs {
|
||||
|
||||
audioFile, err := gomu.playlist.findAudioFile(v)
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
q.enqueue(audioFile)
|
||||
}
|
||||
|
||||
q.updateTitle()
|
||||
}
|
||||
|
||||
// update current playing song name to reflect the changes during rename and paste
|
||||
func (q *Queue) updateCurrentSongName(oldAudio *AudioFile, newAudio *AudioFile) error {
|
||||
|
||||
if !gomu.player.IsRunning() && !gomu.player.IsPaused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
currentSong := gomu.player.GetCurrentSong()
|
||||
position := gomu.playingBar.getProgress()
|
||||
paused := gomu.player.IsPaused()
|
||||
|
||||
if oldAudio.name != currentSong.Name() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// we insert it in the first of queue, then play it
|
||||
gomu.queue.pushFront(newAudio)
|
||||
tmpLoop := q.isLoop
|
||||
q.isLoop = false
|
||||
gomu.player.Skip()
|
||||
gomu.player.Seek(position)
|
||||
if paused {
|
||||
gomu.player.TogglePause()
|
||||
}
|
||||
q.isLoop = tmpLoop
|
||||
q.updateTitle()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// update current playing song path to reflect the changes during rename and paste
|
||||
func (q *Queue) updateCurrentSongPath(oldAudio *AudioFile, newAudio *AudioFile) error {
|
||||
|
||||
if !gomu.player.IsRunning() && !gomu.player.IsPaused() {
|
||||
return nil
|
||||
}
|
||||
|
||||
currentSong := gomu.player.GetCurrentSong()
|
||||
position := gomu.playingBar.getProgress()
|
||||
paused := gomu.player.IsPaused()
|
||||
|
||||
// Here we check the situation when currentsong is under oldAudio folder
|
||||
if !strings.Contains(currentSong.Path(), oldAudio.path) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Here is the handling of folder rename and paste
|
||||
currentSongAudioFile, err := gomu.playlist.findAudioFile(sha1Hex(getName(currentSong.Name())))
|
||||
if err != nil {
|
||||
return tracerr.Wrap(err)
|
||||
}
|
||||
gomu.queue.pushFront(currentSongAudioFile)
|
||||
tmpLoop := q.isLoop
|
||||
q.isLoop = false
|
||||
gomu.player.Skip()
|
||||
gomu.player.Seek(position)
|
||||
if paused {
|
||||
gomu.player.TogglePause()
|
||||
}
|
||||
q.isLoop = tmpLoop
|
||||
|
||||
q.updateTitle()
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// update current playing song simply delete it
|
||||
func (q *Queue) updateCurrentSongDelete(oldAudio *AudioFile) {
|
||||
if !gomu.player.IsRunning() && !gomu.player.IsPaused() {
|
||||
return
|
||||
}
|
||||
|
||||
currentSong := gomu.player.GetCurrentSong()
|
||||
paused := gomu.player.IsPaused()
|
||||
|
||||
var delete bool
|
||||
if oldAudio.isAudioFile {
|
||||
if oldAudio.name == currentSong.Name() {
|
||||
delete = true
|
||||
}
|
||||
} else {
|
||||
if strings.Contains(currentSong.Path(), oldAudio.path) {
|
||||
delete = true
|
||||
}
|
||||
}
|
||||
|
||||
if !delete {
|
||||
return
|
||||
}
|
||||
|
||||
tmpLoop := q.isLoop
|
||||
q.isLoop = false
|
||||
gomu.player.Skip()
|
||||
if paused {
|
||||
gomu.player.TogglePause()
|
||||
}
|
||||
q.isLoop = tmpLoop
|
||||
q.updateTitle()
|
||||
|
||||
}
|
||||
|
41
start.go
41
start.go
@ -71,7 +71,7 @@ func defineInternals() {
|
||||
playlist, _ := gomu.anko.NewModule("Playlist")
|
||||
playlist.Define("get_focused", gomu.playlist.getCurrentFile)
|
||||
playlist.Define("focus", func(filepath string) {
|
||||
|
||||
|
||||
root := gomu.playlist.GetRoot()
|
||||
root.Walk(func(node, _ *tview.TreeNode) bool {
|
||||
|
||||
@ -241,9 +241,11 @@ module General {
|
||||
invidious_instance = "https://vid.puffyan.us"
|
||||
# Prefered language for lyrics to be displayed, if not available, english version
|
||||
# will be displayed.
|
||||
# Available tags: en,el,ko,es,th,vi,zh-Hans,zh-Hant, and can be separated with comma.
|
||||
# Available tags: en,el,ko,es,th,vi,zh-Hans,zh-Hant,zh-CN and can be separated with comma.
|
||||
# find more tags: youtube-dl --skip-download --list-subs "url"
|
||||
lang_lyric = "en"
|
||||
# When save tag, could rename the file by tag info: artist-songname-album
|
||||
rename_bytag = false
|
||||
}
|
||||
|
||||
module Emoji {
|
||||
@ -372,7 +374,9 @@ func start(application *tview.Application, args Args) {
|
||||
}
|
||||
|
||||
audioFile := audio.(*AudioFile)
|
||||
|
||||
gomu.playingBar.newProgress(audioFile, int(duration.Seconds()))
|
||||
|
||||
name := audio.Name()
|
||||
var description string
|
||||
|
||||
@ -392,26 +396,28 @@ func start(application *tview.Application, args Args) {
|
||||
logError(err)
|
||||
}
|
||||
}()
|
||||
|
||||
})
|
||||
|
||||
gomu.player.SetSongFinish(func(currAudio player.Audio) {
|
||||
audio, err := gomu.queue.dequeue()
|
||||
if err != nil {
|
||||
gomu.playingBar.setDefault()
|
||||
return
|
||||
}
|
||||
|
||||
err = gomu.player.Run(audio)
|
||||
if err != nil {
|
||||
die(err)
|
||||
}
|
||||
|
||||
gomu.playingBar.subtitles = nil
|
||||
gomu.playingBar.subtitle = nil
|
||||
if gomu.queue.isLoop {
|
||||
_, err = gomu.queue.enqueue(currAudio.(*AudioFile))
|
||||
if err != nil {
|
||||
logError(err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(gomu.queue.items) > 0 {
|
||||
err := gomu.queue.playQueue()
|
||||
if err != nil {
|
||||
logError(err)
|
||||
}
|
||||
} else {
|
||||
gomu.playingBar.setDefault()
|
||||
}
|
||||
})
|
||||
|
||||
flex := layout(gomu)
|
||||
@ -423,9 +429,7 @@ func start(application *tview.Application, args Args) {
|
||||
|
||||
gomu.playingBar.setDefault()
|
||||
|
||||
isQueueLoop := gomu.anko.GetBool("General.queue_loop")
|
||||
|
||||
gomu.queue.isLoop = isQueueLoop
|
||||
gomu.queue.isLoop = gomu.anko.GetBool("General.queue_loop")
|
||||
|
||||
loadQueue := gomu.anko.GetBool("General.load_prev_queue")
|
||||
|
||||
@ -436,6 +440,12 @@ func start(application *tview.Application, args Args) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(gomu.queue.items) > 0 {
|
||||
if err := gomu.queue.playQueue(); err != nil {
|
||||
logError(err)
|
||||
}
|
||||
}
|
||||
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
@ -526,7 +536,6 @@ func start(application *tview.Application, args Args) {
|
||||
}
|
||||
})
|
||||
|
||||
go populateAudioLength(gomu.playlist.GetRoot())
|
||||
gomu.app.SetRoot(gomu.pages, true).SetFocus(gomu.playlist)
|
||||
|
||||
// main loop
|
||||
|
140
tageditor.go
140
tageditor.go
@ -22,10 +22,9 @@ type lyricFlex struct {
|
||||
*tview.Flex
|
||||
FocusedItem tview.Primitive
|
||||
inputs []tview.Primitive
|
||||
box *tview.Box
|
||||
}
|
||||
|
||||
var box *tview.Box = tview.NewBox()
|
||||
|
||||
// tagPopup is used to edit tag, delete and fetch lyrics
|
||||
func tagPopup(node *AudioFile) (err error) {
|
||||
|
||||
@ -45,9 +44,9 @@ func tagPopup(node *AudioFile) (err error) {
|
||||
deleteLyricButton *tview.Button = tview.NewButton("Delete Lyric")
|
||||
getLyricDropDown *tview.DropDown = tview.NewDropDown()
|
||||
getLyricButton *tview.Button = tview.NewButton("Fetch Lyric")
|
||||
lyricTextView *tview.TextView
|
||||
leftGrid *tview.Grid = tview.NewGrid()
|
||||
rightFlex *tview.Flex = tview.NewFlex()
|
||||
lyricTextView *tview.TextView = tview.NewTextView()
|
||||
leftGrid *tview.Grid = tview.NewGrid()
|
||||
rightFlex *tview.Flex = tview.NewFlex()
|
||||
)
|
||||
|
||||
artistInputField.SetLabel("Artist: ").
|
||||
@ -65,12 +64,20 @@ func tagPopup(node *AudioFile) (err error) {
|
||||
SetText(tag.Album()).
|
||||
SetFieldBackgroundColor(gomu.colors.popup)
|
||||
|
||||
leftBox := tview.NewBox().
|
||||
SetBorder(true).
|
||||
SetTitle(node.name).
|
||||
SetBackgroundColor(gomu.colors.popup).
|
||||
SetBorderColor(gomu.colors.accent).
|
||||
SetTitleColor(gomu.colors.accent).
|
||||
SetBorderPadding(1, 1, 2, 2)
|
||||
|
||||
getTagButton.SetSelectedFunc(func() {
|
||||
var titles []string
|
||||
audioFile := node
|
||||
go func() {
|
||||
var getLyric lyric.GetLyricCn
|
||||
results, err := getLyric.GetLyricOptions(audioFile.name)
|
||||
var lyricFetcher lyric.LyricFetcherCn
|
||||
results, err := lyricFetcher.LyricOptions(audioFile.name)
|
||||
if err != nil {
|
||||
errorPopup(err)
|
||||
return
|
||||
@ -112,8 +119,27 @@ func tagPopup(node *AudioFile) (err error) {
|
||||
errorPopup(err)
|
||||
return
|
||||
}
|
||||
if gomu.anko.GetBool("General.rename_bytag") {
|
||||
newName := fmt.Sprintf("%s-%s", newTag.Artist, newTag.Title)
|
||||
err = gomu.playlist.rename(newName)
|
||||
if err != nil {
|
||||
errorPopup(err)
|
||||
return
|
||||
}
|
||||
gomu.playlist.refresh()
|
||||
leftBox.SetTitle(newName)
|
||||
|
||||
// update queue
|
||||
err = gomu.playlist.refreshAfterRename(node, newName)
|
||||
if err != nil {
|
||||
errorPopup(err)
|
||||
return
|
||||
}
|
||||
node = gomu.playlist.getCurrentFile()
|
||||
}
|
||||
defaultTimedPopup(" Success ", "Tag update successfully")
|
||||
})
|
||||
gomu.app.Draw()
|
||||
}()
|
||||
}()
|
||||
}).
|
||||
@ -130,14 +156,36 @@ func tagPopup(node *AudioFile) (err error) {
|
||||
return
|
||||
}
|
||||
defer tag.Close()
|
||||
tag.SetArtist(artistInputField.GetText())
|
||||
tag.SetTitle(titleInputField.GetText())
|
||||
tag.SetAlbum(albumInputField.GetText())
|
||||
newArtist := artistInputField.GetText()
|
||||
newTitle := titleInputField.GetText()
|
||||
newAlbum := albumInputField.GetText()
|
||||
tag.SetArtist(newArtist)
|
||||
tag.SetTitle(newTitle)
|
||||
tag.SetAlbum(newAlbum)
|
||||
err = tag.Save()
|
||||
if err != nil {
|
||||
errorPopup(err)
|
||||
return
|
||||
}
|
||||
if gomu.anko.GetBool("General.rename_bytag") {
|
||||
newName := fmt.Sprintf("%s-%s", newArtist, newTitle)
|
||||
err = gomu.playlist.rename(newName)
|
||||
if err != nil {
|
||||
errorPopup(err)
|
||||
return
|
||||
}
|
||||
gomu.playlist.refresh()
|
||||
leftBox.SetTitle(newName)
|
||||
|
||||
// update queue
|
||||
err = gomu.playlist.refreshAfterRename(node, newName)
|
||||
if err != nil {
|
||||
errorPopup(err)
|
||||
return
|
||||
}
|
||||
node = gomu.playlist.getCurrentFile()
|
||||
}
|
||||
|
||||
defaultTimedPopup(" Success ", "Tag update successfully")
|
||||
|
||||
}).
|
||||
@ -259,22 +307,24 @@ func tagPopup(node *AudioFile) (err error) {
|
||||
|
||||
options = newOptions
|
||||
// Update dropdown options
|
||||
lyricDropDown.SetOptions(newOptions, nil).
|
||||
SetCurrentOption(0).
|
||||
SetSelectedFunc(func(text string, _ int) {
|
||||
lyricTextView.SetText(popupLyricMap[text]).
|
||||
SetTitle(" " + text + " lyric preview ")
|
||||
})
|
||||
gomu.app.QueueUpdateDraw(func() {
|
||||
lyricDropDown.SetOptions(newOptions, nil).
|
||||
SetCurrentOption(0).
|
||||
SetSelectedFunc(func(text string, _ int) {
|
||||
lyricTextView.SetText(popupLyricMap[text]).
|
||||
SetTitle(" " + text + " lyric preview ")
|
||||
})
|
||||
|
||||
// Update lyric preview
|
||||
if len(newOptions) > 0 {
|
||||
_, langExt := lyricDropDown.GetCurrentOption()
|
||||
lyricTextView.SetText(popupLyricMap[langExt]).
|
||||
SetTitle(" " + langExt + " lyric preview ")
|
||||
} else {
|
||||
lyricTextView.SetText("No lyric embeded.").
|
||||
SetTitle(" lyric preview ")
|
||||
}
|
||||
// Update lyric preview
|
||||
if len(newOptions) > 0 {
|
||||
_, langExt := lyricDropDown.GetCurrentOption()
|
||||
lyricTextView.SetText(popupLyricMap[langExt]).
|
||||
SetTitle(" " + langExt + " lyric preview ")
|
||||
} else {
|
||||
lyricTextView.SetText("No lyric embeded.").
|
||||
SetTitle(" lyric preview ")
|
||||
}
|
||||
})
|
||||
}()
|
||||
}).
|
||||
SetBackgroundColorActivated(gomu.colors.popup).
|
||||
@ -290,7 +340,7 @@ func tagPopup(node *AudioFile) (err error) {
|
||||
lyricText = "No lyric embeded."
|
||||
langExt = ""
|
||||
}
|
||||
lyricTextView = tview.NewTextView()
|
||||
|
||||
lyricTextView.
|
||||
SetDynamicColors(true).
|
||||
SetRegions(true).
|
||||
@ -304,7 +354,9 @@ func tagPopup(node *AudioFile) (err error) {
|
||||
SetWrap(true).
|
||||
SetBorder(true)
|
||||
lyricTextView.SetChangedFunc(func() {
|
||||
lyricTextView.ScrollToBeginning()
|
||||
gomu.app.QueueUpdate(func() {
|
||||
lyricTextView.ScrollToBeginning()
|
||||
})
|
||||
})
|
||||
|
||||
leftGrid.SetRows(3, 1, 3, 3, 3, 3, 0, 3, 3, 1, 3, 3).
|
||||
@ -319,15 +371,6 @@ func tagPopup(node *AudioFile) (err error) {
|
||||
AddItem(lyricDropDown, 10, 0, 1, 3, 1, 10, true).
|
||||
AddItem(deleteLyricButton, 11, 0, 1, 3, 1, 10, true)
|
||||
|
||||
box.SetBorder(true).
|
||||
SetTitle(node.name).
|
||||
SetBackgroundColor(gomu.colors.popup).
|
||||
SetBorderColor(gomu.colors.accent).
|
||||
SetTitleColor(gomu.colors.accent).
|
||||
SetBorderPadding(1, 1, 2, 2)
|
||||
|
||||
leftGrid.Box = box
|
||||
|
||||
rightFlex.SetDirection(tview.FlexColumn).
|
||||
AddItem(lyricTextView, 0, 1, true)
|
||||
|
||||
@ -337,8 +380,11 @@ func tagPopup(node *AudioFile) (err error) {
|
||||
AddItem(rightFlex, 0, 3, true),
|
||||
nil,
|
||||
nil,
|
||||
leftBox,
|
||||
}
|
||||
|
||||
leftGrid.Box = lyricFlex.box
|
||||
|
||||
lyricFlex.inputs = []tview.Primitive{
|
||||
getTagButton,
|
||||
artistInputField,
|
||||
@ -352,6 +398,10 @@ func tagPopup(node *AudioFile) (err error) {
|
||||
lyricTextView,
|
||||
}
|
||||
|
||||
if gomu.playingBar.albumPhoto != nil {
|
||||
gomu.playingBar.albumPhoto.Clear()
|
||||
}
|
||||
|
||||
gomu.pages.AddPage(popupID, center(lyricFlex, 90, 36), true, true)
|
||||
gomu.popups.push(lyricFlex)
|
||||
|
||||
@ -362,13 +412,13 @@ func tagPopup(node *AudioFile) (err error) {
|
||||
case tcell.KeyEsc:
|
||||
gomu.pages.RemovePage(popupID)
|
||||
gomu.popups.pop()
|
||||
case tcell.KeyTab:
|
||||
case tcell.KeyTab, tcell.KeyCtrlN, tcell.KeyCtrlJ:
|
||||
lyricFlex.cycleFocus(gomu.app, false)
|
||||
case tcell.KeyBacktab:
|
||||
case tcell.KeyBacktab, tcell.KeyCtrlP, tcell.KeyCtrlK:
|
||||
lyricFlex.cycleFocus(gomu.app, true)
|
||||
case tcell.KeyRight:
|
||||
case tcell.KeyDown:
|
||||
lyricFlex.cycleFocus(gomu.app, false)
|
||||
case tcell.KeyLeft:
|
||||
case tcell.KeyUp:
|
||||
lyricFlex.cycleFocus(gomu.app, true)
|
||||
}
|
||||
|
||||
@ -409,12 +459,12 @@ func (f *lyricFlex) cycleFocus(app *tview.Application, reverse bool) {
|
||||
if f.inputs[9].HasFocus() {
|
||||
f.inputs[9].(*tview.TextView).SetBorderColor(gomu.colors.accent).
|
||||
SetTitleColor(gomu.colors.accent)
|
||||
box.SetBorderColor(gomu.colors.background).
|
||||
f.box.SetBorderColor(gomu.colors.background).
|
||||
SetTitleColor(gomu.colors.background)
|
||||
} else {
|
||||
f.inputs[9].(*tview.TextView).SetBorderColor(gomu.colors.background).
|
||||
SetTitleColor(gomu.colors.background)
|
||||
box.SetBorderColor(gomu.colors.accent).
|
||||
f.box.SetBorderColor(gomu.colors.accent).
|
||||
SetTitleColor(gomu.colors.accent)
|
||||
}
|
||||
return
|
||||
@ -433,12 +483,12 @@ func (f *lyricFlex) Focus(delegate func(p tview.Primitive)) {
|
||||
}
|
||||
|
||||
// loadTagMap will load from tag and return a map of langExt to lyrics
|
||||
func (node *AudioFile) loadTagMap() (tag *id3v2.Tag, popupLyricMap map[string]string, options []string, err error) {
|
||||
func (a *AudioFile) loadTagMap() (tag *id3v2.Tag, popupLyricMap map[string]string, options []string, err error) {
|
||||
|
||||
popupLyricMap = make(map[string]string)
|
||||
|
||||
if node.isAudioFile {
|
||||
tag, err = id3v2.Open(node.path, id3v2.Options{Parse: true})
|
||||
if a.isAudioFile {
|
||||
tag, err = id3v2.Open(a.path, id3v2.Options{Parse: true})
|
||||
if err != nil {
|
||||
return nil, nil, nil, tracerr.Wrap(err)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user