first commit

This commit is contained in:
Drew Weymouth
2022-12-17 16:52:13 -08:00
parent cd1f795bb4
commit c21d7d03eb
16 changed files with 2069 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
package backend
import (
"log"
"strconv"
"github.com/bluele/gcache"
subsonic "github.com/dweymouth/go-subsonic"
)
type AlbumIterator interface {
Next() *subsonic.AlbumID3
NextN(int, func(*subsonic.AlbumID3))
}
type LibraryManager struct {
s *subsonic.Client
albumCache gcache.Cache
}
func NewLibraryManager(s *subsonic.Client) *LibraryManager {
cache := gcache.New(250).LRU().Build()
return &LibraryManager{
s: s,
albumCache: cache,
}
}
func (l *LibraryManager) RecentlyAddedIter() AlbumIterator {
return l.newBaseIter("newest")
}
func (l *LibraryManager) RecentlyPlayedIter() AlbumIterator {
return l.newBaseIter("recent")
}
func (l *LibraryManager) StarredIter() AlbumIterator {
return l.newBaseIter("starred")
}
func (l *LibraryManager) FrequentlyPlayedIter() AlbumIterator {
return l.newBaseIter("frequent")
}
func (l *LibraryManager) CacheAlbum(a *subsonic.AlbumID3) {
l.albumCache.Set(a.ID, a)
}
func (l *LibraryManager) GetAlbum(id string) (*subsonic.AlbumID3, error) {
if l.albumCache.Has(id) {
if a, err := l.albumCache.Get(id); err == nil {
return a.(*subsonic.AlbumID3), nil
}
}
a, err := l.s.GetAlbum(id)
if err != nil {
return nil, err
}
l.albumCache.Set(a.ID, a)
return a, nil
}
type baseIter struct {
listType string
pos int
l *LibraryManager
s *subsonic.Client
prefetched []*subsonic.AlbumID3
prefetchedPos int
done bool
}
func (l *LibraryManager) newBaseIter(listType string) *baseIter {
return &baseIter{
listType: listType,
l: l,
s: l.s,
}
}
// TODO: figure out why the iterator sometimes returns an album twice
func (r *baseIter) Next() *subsonic.AlbumID3 {
if r.done {
return nil
}
if r.prefetched != nil {
a := r.prefetched[r.prefetchedPos]
r.prefetchedPos++
if r.prefetchedPos == len(r.prefetched) {
r.prefetched = nil
r.prefetchedPos = 0
}
r.pos++
r.l.CacheAlbum(a)
return a
}
albums, err := r.s.GetAlbumList2(r.listType, map[string]string{"size": "20", "offset": strconv.Itoa(r.pos)})
if err != nil {
log.Println(err)
albums = nil
}
if len(albums) == 0 {
r.done = true
return nil
} else if len(albums) == 1 {
r.l.CacheAlbum(albums[0])
r.done = true
return albums[0]
}
r.prefetched = albums
r.prefetchedPos = 1
r.l.CacheAlbum(r.prefetched[0])
return r.prefetched[0]
}
func (r *baseIter) NextN(n int, cb func(*subsonic.AlbumID3)) {
go func() {
for i := 0; i < n; i++ {
a := r.Next()
cb(a)
if a == nil {
break
}
}
}()
}
+36
View File
@@ -0,0 +1,36 @@
package backend
import (
"image"
"github.com/bluele/gcache"
subsonic "github.com/dweymouth/go-subsonic"
)
type ImageManager struct {
s *subsonic.Client
thumbnailCache gcache.Cache
}
func NewImageManager(s *subsonic.Client) *ImageManager {
cache := gcache.New(100).LRU().Build()
return &ImageManager{
s: s,
thumbnailCache: cache,
}
}
func (i *ImageManager) GetAlbumThumbnail(albumID string) (image.Image, error) {
if i.thumbnailCache.Has(albumID) {
if img, err := i.thumbnailCache.Get(albumID); err == nil {
return img.(image.Image), nil
}
}
// TODO: on disc cache
img, err := i.s.GetCoverArt(albumID, map[string]string{"size": "250"})
if err != nil {
return nil, err
}
i.thumbnailCache.Set(albumID, img)
return img, nil
}
+154
View File
@@ -0,0 +1,154 @@
package backend
import (
"context"
"gomuse/player"
"time"
subsonic "github.com/dweymouth/go-subsonic"
)
type PlaybackManager struct {
ctx context.Context
cancelPollPos context.CancelFunc
pollingTick *time.Ticker
client *subsonic.Client
player *player.Player
playQueue []*subsonic.Child
nowPlayingIdx int64
onSongChange []func(*subsonic.Child)
onPlayTimeUpdate []func(float64, float64)
}
func NewPlaybackManager(ctx context.Context, cli *subsonic.Client, p *player.Player) *PlaybackManager {
pm := &PlaybackManager{
ctx: ctx,
client: cli,
player: p,
}
p.OnTrackChange(func(tracknum int64) {
pm.nowPlayingIdx = tracknum
for _, cb := range pm.onSongChange {
cb(pm.NowPlaying())
}
if pm.pollingTick != nil {
pm.pollingTick.Reset(pm.getPollSpeed())
}
})
p.OnSeek(func() {
pm.doUpdateTimePos()
})
p.OnStopped(func() {
pm.stopPollTimePos()
for _, cb := range pm.onSongChange {
cb(nil)
}
})
p.OnPaused(func() {
pm.stopPollTimePos()
})
p.OnPlaying(func() {
pm.startPollTimePos()
})
return pm
}
func (p *PlaybackManager) IsSeeking() bool {
return p.player.IsSeeking()
}
// Gets the curently playing song, if any.
func (p *PlaybackManager) NowPlaying() *subsonic.Child {
if len(p.playQueue) == 0 || p.player.GetStatus().State == player.Stopped {
return nil
}
// TODO: somehow ran into an index out of range crash here
return p.playQueue[p.nowPlayingIdx]
}
// Sets a callback that is notified whenever a new song begins playing.
func (p *PlaybackManager) OnSongChange(cb func(*subsonic.Child)) {
p.onSongChange = append(p.onSongChange, cb)
}
// Registers a callback that is notified whenever the play time should be updated.
func (p *PlaybackManager) OnPlayTimeUpdate(cb func(float64, float64)) {
p.onPlayTimeUpdate = append(p.onPlayTimeUpdate, cb)
}
// Loads the specified album into the play queue.
func (p *PlaybackManager) LoadAlbum(albumID string, appendToQueue bool) error {
album, err := p.client.GetAlbum(albumID)
if err != nil {
return err
}
if !appendToQueue {
p.player.Stop()
p.playQueue = nil
}
for _, song := range album.Song {
url, err := p.client.GetStreamURL(song.ID, map[string]string{})
if err != nil {
return err
}
p.player.AppendFile(url.String())
p.playQueue = append(p.playQueue, song)
}
return nil
}
func (p *PlaybackManager) PlayAlbum(albumID string) error {
if err := p.LoadAlbum(albumID, false); err != nil {
return err
}
return p.player.PlayFromBeginning()
}
// depending on the length of the current track, we need to poll
// faster or less fast to make track position scroll bar look smooth
func (p *PlaybackManager) getPollSpeed() time.Duration {
t := p.player.GetStatus().Duration
if t < 30 {
return 100 * time.Millisecond
} else if t < 90 {
return 150 * time.Millisecond
} else if t < 120 {
return 250 * time.Millisecond
} else {
return 333 * time.Millisecond
}
}
func (p *PlaybackManager) startPollTimePos() {
ctx, cancel := context.WithCancel(p.ctx)
p.cancelPollPos = cancel
p.pollingTick = time.NewTicker(p.getPollSpeed())
go func() {
for {
select {
case <-ctx.Done():
p.pollingTick.Stop()
p.pollingTick = nil
return
case <-p.pollingTick.C:
p.doUpdateTimePos()
}
}
}()
}
func (p *PlaybackManager) doUpdateTimePos() {
s := p.player.GetStatus()
for _, cb := range p.onPlayTimeUpdate {
cb(s.TimePos, s.Duration)
}
}
func (p *PlaybackManager) stopPollTimePos() {
if p.cancelPollPos != nil {
p.cancelPollPos()
p.cancelPollPos = nil
}
}