dndmusicbot/player.go

288 lines
5.3 KiB
Go
Raw Permalink Normal View History

package main
import (
"container/list"
"context"
"dndmusicbot/reisenstreamer"
"encoding/json"
"errors"
"log"
"net/http"
"sync"
"time"
"github.com/avast/retry-go/v4"
"github.com/google/uuid"
"github.com/gopxl/beep"
"github.com/julienschmidt/httprouter"
"github.com/kataras/go-events"
ytdl "github.com/kkdai/youtube/v2"
)
var player *Player
func init() {
player = new(Player)
player.playlist = list.New()
}
type Player struct {
sync.RWMutex
current *list.Element
playlist *list.List
PlayList *ytdl.Playlist
PlaylistID uuid.UUID
Playing bool
ctx context.Context
cancel context.CancelFunc
}
type PlayerItem struct {
Artist string
Title string
ID string
Uri string
Position time.Duration
Duration time.Duration
sr beep.SampleRate
streamer *reisenstreamer.Streamer
fn beep.StreamerFunc
ctx context.Context
cancel context.CancelFunc
Loaded bool
Idx int
}
func (i *PlayerItem) Load() error {
if i.streamer != nil {
return nil
}
i.ctx, i.cancel = context.WithCancel(context.Background())
stream, err := retry.DoWithData(func() (*reisenstreamer.Streamer, error) {
out := reisenstreamer.New(i.ctx, i.Uri)
if i.ctx.Err() != nil {
return nil, retry.Unrecoverable(i.ctx.Err())
}
if out == nil {
return nil, errors.New("streamer is nil")
}
return out, nil
})
if err != nil {
log.Println(err)
return err
}
i.streamer = stream
i.fn = stream.StreamerFunc()
i.sr = beep.SampleRate(stream.SampleRate)
i.Loaded = true
return nil
}
func (i *PlayerItem) Unload() {
i.Loaded = false
i.Position = 0
if i.cancel != nil {
i.cancel()
}
if i.streamer != nil {
i.streamer.Close()
}
i.fn = nil
i.streamer = nil
}
// TODO use context here to cancel this if someone loads two lists..
func (p *Player) LoadPlaylist(id uuid.UUID, playlist *ytdl.Playlist) {
if p.cancel != nil {
p.cancel()
}
if p.Playing {
p.Stop()
}
p.PlaylistID = id
p.PlayList = playlist
p.ctx, p.cancel = context.WithCancel(context.Background())
for i, info := range playlist.Videos {
select {
case <-p.ctx.Done():
log.Printf("LoadPlaylist cancelled: %v\n", p.ctx.Err())
return
default:
item := new(PlayerItem)
item.Artist = info.Author
item.Title = info.Title
item.Duration = info.Duration
item.ID = info.ID
item.Uri = "http://localhost:" + config.GetString("web.port") + "/youtube/" + info.ID
item.Idx = i + 1
p.playlist.PushBack(item)
}
}
p.Next()
}
func (p *Player) Stop() {
if !p.Playing {
return
}
p.Playing = false
p.PlaylistID = uuid.UUID{}
app.events.Emit(events.EventName("player"))
if p.current != nil {
p.current.Value.(*PlayerItem).cancel()
p.NextValue().Value.(*PlayerItem).cancel()
}
p.current = nil
p.playlist = p.playlist.Init()
}
func (p *Player) Play(idx int) {
var i int
var e *list.Element
for e = p.playlist.Front(); e != nil; e = e.Next() {
if idx == i {
break
}
i++
}
if e != nil {
e.Value.(*PlayerItem).Load()
cur := p.current
next := p.NextValue()
p.current = e
p.NextValue().Value.(*PlayerItem).Load()
app.events.Emit(events.EventName("player"))
cur.Value.(*PlayerItem).Unload()
next.Value.(*PlayerItem).Unload()
}
}
func (p *Player) NextValue() *list.Element {
switch {
case p.current == nil:
fallthrough
case p.current.Next() == nil:
return p.playlist.Front()
default:
return p.current.Next()
}
}
func (p *Player) PrevValue() *list.Element {
if p.current.Prev() != nil {
return p.current.Prev()
} else {
return p.playlist.Back()
}
}
func (p *Player) Next() {
p.current = p.NextValue()
p.PrevValue().Value.(*PlayerItem).Unload()
p.Playing = true
app.events.Emit(events.EventName("player"))
item := p.current.Value.(*PlayerItem)
err := item.Load()
if err != nil {
log.Println(err)
return
}
// Preload next song.
next := p.NextValue()
err = next.Value.(*PlayerItem).Load()
if err != nil {
log.Println(err)
return
}
}
func (p *Player) Prev() {
p.current = p.PrevValue()
p.Playing = true
app.events.Emit(events.EventName("player"))
item := p.current.Value.(*PlayerItem)
err := item.Load()
if err != nil {
log.Println(err)
return
}
// Reload next song
next := p.NextValue().Value.(*PlayerItem)
next.Unload()
next.Load()
// Unload nextnext song.
if p.NextValue().Next() == nil {
p.playlist.Front().Value.(*PlayerItem).Unload()
} else {
p.NextValue().Next().Value.(*PlayerItem).Unload()
}
}
func (p *Player) Stream(samples [][2]float64) (n int, ok bool) {
if !p.Playing || p.current.Value.(*PlayerItem).streamer == nil || !p.current.Value.(*PlayerItem).streamer.Started {
for i := 0; i < len(samples); i++ {
samples[i] = [2]float64{}
}
return len(samples), true
}
item := p.current.Value.(*PlayerItem)
n, ok = item.fn.Stream(samples)
item.Position = item.sr.D(item.sr.N(item.Position) + n)
if !ok {
go p.Next()
ok = true
return
}
return
}
func (p *Player) Err() error {
return nil
}
func CurrentPlaylist(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
w.Header().Add("Content-Type", "application/json")
if player.PlayList == nil {
json.NewEncoder(w).Encode(nil)
return
}
err := json.NewEncoder(w).Encode(player.PlayList.Videos)
if err != nil {
http.Error(w, "Unable to get playlist. "+err.Error(), http.StatusInternalServerError)
}
}