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