Merge pull request #51 from tramhao/master

Fix race conditions. Add thumbnail preview. Fix search bar freezes whole UI
This commit is contained in:
Raziman Mahathir 2021-04-18 08:50:25 +08:00 committed by GitHub
commit 3c95c94980
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 721 additions and 301 deletions

View File

@ -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 :)

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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)
}

View File

@ -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())}
}

View File

@ -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
}

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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)

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
View File

@ -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()
}

View File

@ -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

View File

@ -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)
}

View File

@ -6,8 +6,6 @@ import (
"bytes"
"errors"
"fmt"
// "io"
"log"
"net/http"
"os"