Greatly simplified song queue data structure

This commit is contained in:
Matthieu Grieger 2015-02-12 13:27:04 -08:00
parent 24fb620e1f
commit b6a6da718d
6 changed files with 134 additions and 200 deletions

View file

@ -1,8 +1,9 @@
MumbleDJ Changelog
==================
### February 11, 2015 -- `v2.4.4`
### February 11, 2015 -- `v2.5.0`
* Updated dependencies and fixed code to match `gumble` API changes.
* Greatly simplified the song queue data structure. Some new bugs could potentially have arisen. Let me know if you find any!
### February 9, 2015 -- `v2.4.3`
* Added configuration option in `mumbledj.gcfg` for default bot comment.

View file

@ -169,15 +169,15 @@ func add(user *gumble.User, username, url string) {
}
if matchFound {
if newSong, err := NewSong(username, shortUrl); err == nil {
if err := dj.queue.AddItem(newSong); err == nil {
if newSong, err := NewSong(username, shortUrl, nil); err == nil {
if err := dj.queue.AddSong(newSong); err == nil {
dj.client.Self.Channel.Send(fmt.Sprintf(SONG_ADDED_HTML, username, newSong.title), false)
if dj.queue.Len() == 1 && !dj.audioStream.IsPlaying() {
if err := dj.queue.CurrentItem().(*Song).Download(); err == nil {
dj.queue.CurrentItem().(*Song).Play()
if err := dj.queue.CurrentSong().Download(); err == nil {
dj.queue.CurrentSong().Play()
} else {
dj.SendPrivateMessage(user, AUDIO_FAIL_MSG)
dj.queue.CurrentItem().(*Song).Delete()
dj.queue.CurrentSong().Delete()
}
}
}
@ -191,16 +191,15 @@ func add(user *gumble.User, username, url string) {
if re.MatchString(url) {
if dj.HasPermission(username, dj.conf.Permissions.AdminAddPlaylists) {
shortUrl = re.FindStringSubmatch(url)[1]
oldLength := dj.queue.Len()
if newPlaylist, err := NewPlaylist(username, shortUrl); err == nil {
if dj.queue.AddItem(newPlaylist); err == nil {
dj.client.Self.Channel.Send(fmt.Sprintf(PLAYLIST_ADDED_HTML, username, newPlaylist.title), false)
if dj.queue.Len() == 1 && !dj.audioStream.IsPlaying() {
if err := dj.queue.CurrentItem().(*Playlist).songs.CurrentItem().(*Song).Download(); err == nil {
dj.queue.CurrentItem().(*Playlist).songs.CurrentItem().(*Song).Play()
} else {
dj.SendPrivateMessage(user, AUDIO_FAIL_MSG)
dj.queue.CurrentItem().(*Playlist).songs.CurrentItem().(*Song).Delete()
}
dj.client.Self.Channel.Send(fmt.Sprintf(PLAYLIST_ADDED_HTML, username, newPlaylist.title), false)
if oldLength == 0 && !dj.audioStream.IsPlaying() {
if err := dj.queue.CurrentSong().Download(); err == nil {
dj.queue.CurrentSong().Play()
} else {
dj.SendPrivateMessage(user, AUDIO_FAIL_MSG)
dj.queue.CurrentSong().Delete()
}
}
} else {
@ -222,15 +221,28 @@ func add(user *gumble.User, username, url string) {
func skip(user *gumble.User, username string, admin, playlistSkip bool) {
if dj.audioStream.IsPlaying() {
if playlistSkip {
if dj.queue.CurrentItem().ItemType() == "playlist" {
if err := dj.queue.CurrentItem().AddSkip(username); err == nil {
if dj.queue.CurrentSong().playlist != nil {
if err := dj.queue.CurrentSong().playlist.AddSkip(username); err == nil {
if admin {
dj.client.Self.Channel.Send(ADMIN_PLAYLIST_SKIP_MSG, false)
} else {
dj.client.Self.Channel.Send(fmt.Sprintf(PLAYLIST_SKIP_ADDED_HTML, username), false)
}
if dj.queue.CurrentItem().SkipReached(len(dj.client.Self.Channel.Users)) || admin {
dj.queue.CurrentItem().(*Playlist).skipped = true
if dj.queue.CurrentSong().playlist.SkipReached(len(dj.client.Self.Channel.Users)) || admin {
id := dj.queue.CurrentSong().playlist.id
dj.queue.CurrentSong().playlist.DeleteSkippers()
for i := 0; i < len(dj.queue.queue); i++ {
if dj.queue.queue[i].playlist != nil {
if dj.queue.queue[i].playlist.id == id {
dj.queue.queue = append(dj.queue.queue[:i], dj.queue.queue[i+1:]...)
i--
}
}
}
if dj.queue.Len() != 0 {
// Set dontSkip to true to avoid audioStream.Stop() callback skipping the new first song.
dj.queue.CurrentSong().dontSkip = true
}
dj.client.Self.Channel.Send(PLAYLIST_SKIPPED_HTML, false)
if err := dj.audioStream.Stop(); err != nil {
panic(errors.New("An error occurred while stopping the current song."))
@ -241,19 +253,13 @@ func skip(user *gumble.User, username string, admin, playlistSkip bool) {
dj.SendPrivateMessage(user, NO_PLAYLIST_PLAYING_MSG)
}
} else {
var currentItem QueueItem
if dj.queue.CurrentItem().ItemType() == "playlist" {
currentItem = dj.queue.CurrentItem().(*Playlist).songs.CurrentItem()
} else {
currentItem = dj.queue.CurrentItem()
}
if err := currentItem.AddSkip(username); err == nil {
if err := dj.queue.CurrentSong().AddSkip(username); err == nil {
if admin {
dj.client.Self.Channel.Send(ADMIN_SONG_SKIP_MSG, false)
} else {
dj.client.Self.Channel.Send(fmt.Sprintf(SKIP_ADDED_HTML, username), false)
}
if currentItem.SkipReached(len(dj.client.Self.Channel.Users)) || admin {
if dj.queue.CurrentSong().SkipReached(len(dj.client.Self.Channel.Users)) || admin {
dj.client.Self.Channel.Send(SONG_SKIPPED_HTML, false)
if err := dj.audioStream.Stop(); err != nil {
panic(errors.New("An error occurred while stopping the current song."))
@ -334,7 +340,7 @@ func reset(username string) {
// the number of songs in the queue to chat.
func numSongs() {
songCount := 0
dj.queue.Traverse(func(i int, item QueueItem) {
dj.queue.Traverse(func(i int, song *Song) {
songCount += 1
})
dj.client.Self.Channel.Send(fmt.Sprintf(NUM_SONGS_HTML, songCount), false)
@ -355,17 +361,11 @@ func nextSong(user *gumble.User) {
// information about the song currently playing.
func currentSong(user *gumble.User) {
if dj.audioStream.IsPlaying() {
var currentItem *Song
if dj.queue.CurrentItem().ItemType() == "playlist" {
currentItem = dj.queue.CurrentItem().(*Playlist).songs.CurrentItem().(*Song)
if dj.queue.CurrentSong().playlist == nil {
dj.SendPrivateMessage(user, fmt.Sprintf(CURRENT_SONG_HTML, dj.queue.CurrentSong().title, dj.queue.CurrentSong().submitter))
} else {
currentItem = dj.queue.CurrentItem().(*Song)
}
if currentItem.playlistTitle == "" {
dj.SendPrivateMessage(user, fmt.Sprintf(CURRENT_SONG_HTML, currentItem.title, currentItem.submitter))
} else {
dj.SendPrivateMessage(user, fmt.Sprintf(CURRENT_SONG_PLAYLIST_HTML, currentItem.title,
currentItem.submitter, currentItem.playlistTitle))
dj.SendPrivateMessage(user, fmt.Sprintf(CURRENT_SONG_PLAYLIST_HTML, dj.queue.CurrentSong().title,
dj.queue.CurrentSong().submitter, dj.queue.CurrentSong().playlist.title))
}
} else {
dj.SendPrivateMessage(user, NO_MUSIC_PLAYING_MSG)

14
main.go
View file

@ -29,6 +29,7 @@ type mumbledj struct {
queue *SongQueue
audioStream *gumble_ffmpeg.Stream
homeDir string
playlistSkips map[string][]string
}
// OnConnect event. First moves MumbleDJ into the default channel specified
@ -84,12 +85,10 @@ func (dj *mumbledj) OnTextMessage(e *gumble.TextMessageEvent) {
func (dj *mumbledj) OnUserChange(e *gumble.UserChangeEvent) {
if e.Type.Has(gumble.UserChangeDisconnected) {
if dj.audioStream.IsPlaying() {
if dj.queue.CurrentItem().ItemType() == "playlist" {
dj.queue.CurrentItem().(*Playlist).RemoveSkip(e.User.Name)
dj.queue.CurrentItem().(*Playlist).songs.CurrentItem().(*Song).RemoveSkip(e.User.Name)
} else {
dj.queue.CurrentItem().(*Song).RemoveSkip(e.User.Name)
if dj.queue.CurrentSong().playlist != nil {
dj.queue.CurrentSong().playlist.RemoveSkip(e.User.Name)
}
dj.queue.CurrentSong().RemoveSkip(e.User.Name)
}
}
}
@ -119,8 +118,9 @@ func (dj *mumbledj) SendPrivateMessage(user *gumble.User, message string) {
// dj variable declaration. This is done outside of main() to allow global use.
var dj = mumbledj{
keepAlive: make(chan bool),
queue: NewSongQueue(),
keepAlive: make(chan bool),
queue: NewSongQueue(),
playlistSkips: make(map[string][]string),
}
// Main function, but only really performs startup tasks. Grabs and parses commandline

View file

@ -20,18 +20,13 @@ import (
// Playlist type declaration.
type Playlist struct {
songs *SongQueue
youtubeId string
title string
submitter string
skippers []string
skipped bool
id string
title string
}
// Returns a new Playlist type. Before returning the new type, the playlist's metadata is collected
// via the YouTube Gdata API.
func NewPlaylist(user, id string) (*Playlist, error) {
queue := NewSongQueue()
jsonUrl := fmt.Sprintf("http://gdata.youtube.com/feeds/api/playlists/%s?v=2&alt=jsonc&maxresults=25", id)
jsonString := ""
@ -59,6 +54,11 @@ func NewPlaylist(user, id string) (*Playlist, error) {
playlistItems = 25
}
playlist := &Playlist{
id: id,
title: playlistTitle,
}
for i := 0; i < playlistItems; i++ {
index := strconv.Itoa(i)
songTitle, _ := jq.String("data", "items", index, "video", "title")
@ -67,63 +67,55 @@ func NewPlaylist(user, id string) (*Playlist, error) {
duration, _ := jq.Int("data", "items", index, "video", "duration")
songDuration := fmt.Sprintf("%d:%02d", duration/60, duration%60)
newSong := &Song{
submitter: user,
title: songTitle,
playlistTitle: playlistTitle,
youtubeId: songId,
playlistId: id,
duration: songDuration,
thumbnailUrl: songThumbnail,
submitter: user,
title: songTitle,
youtubeId: songId,
duration: songDuration,
thumbnailUrl: songThumbnail,
playlist: playlist,
dontSkip: false,
}
queue.AddItem(newSong)
dj.queue.AddSong(newSong)
}
playlist := &Playlist{
songs: queue,
youtubeId: id,
title: playlistTitle,
submitter: user,
skipped: false,
}
return playlist, nil
}
// Adds a skip to the skippers slice. If the user is already in the slice AddSkip() returns
// an error and does not add a duplicate skip.
// Adds a skip to the skippers slice for the current playlist.
func (p *Playlist) AddSkip(username string) error {
for _, user := range p.skippers {
for _, user := range dj.playlistSkips[p.id] {
if username == user {
return errors.New("This user has already skipped the current song.")
}
}
p.skippers = append(p.skippers, username)
dj.playlistSkips[p.id] = append(dj.playlistSkips[p.id], username)
return nil
}
// Removes a skip from the skippers slice. If username is not in the slice, an error is
// returned.
func (p *Playlist) RemoveSkip(username string) error {
for i, user := range p.skippers {
for i, user := range dj.playlistSkips[p.id] {
if username == user {
p.skippers = append(p.skippers[:i], p.skippers[i+1:]...)
dj.playlistSkips[p.id] = append(dj.playlistSkips[p.id][:i], dj.playlistSkips[p.id][i+1:]...)
return nil
}
}
return errors.New("This user has not skipped the song.")
}
// Removes skippers entry in dj.playlistSkips.
func (p *Playlist) DeleteSkippers() {
delete(dj.playlistSkips, p.id)
}
// Calculates current skip ratio based on number of users within MumbleDJ's channel and the
// amount of values in the skippers slice. If the value is greater than or equal to the skip ratio
// defined in mumbledj.gcfg, the function returns true. Returns false otherwise.
func (p *Playlist) SkipReached(channelUsers int) bool {
if float32(len(p.skippers))/float32(channelUsers) >= dj.conf.General.PlaylistSkipRatio {
if float32(len(dj.playlistSkips[p.id]))/float32(channelUsers) >= dj.conf.General.PlaylistSkipRatio {
return true
} else {
return false
}
}
// Returns "playlist" as the item type. Used for differentiating Songs from Playlists.
func (p *Playlist) ItemType() string {
return "playlist"
}

45
song.go
View file

@ -21,20 +21,19 @@ import (
// Song type declaration.
type Song struct {
submitter string
title string
playlistTitle string
youtubeId string
playlistId string
duration string
thumbnailUrl string
itemType string
skippers []string
submitter string
title string
youtubeId string
duration string
thumbnailUrl string
skippers []string
playlist *Playlist
dontSkip bool
}
// Returns a new Song type. Before returning the new type, the song's metadata is collected
// via the YouTube Gdata API.
func NewSong(user, id string) (*Song, error) {
func NewSong(user, id string, playlist *Playlist) (*Song, error) {
jsonUrl := fmt.Sprintf("http://gdata.youtube.com/feeds/api/videos/%s?v=2&alt=jsonc", id)
jsonString := ""
@ -62,14 +61,13 @@ func NewSong(user, id string) (*Song, error) {
videoDuration := fmt.Sprintf("%d:%02d", duration/60, duration%60)
song := &Song{
submitter: user,
title: videoTitle,
playlistTitle: "",
youtubeId: id,
playlistId: "",
duration: videoDuration,
thumbnailUrl: videoThumbnail,
itemType: "song",
submitter: user,
title: videoTitle,
youtubeId: id,
duration: videoDuration,
thumbnailUrl: videoThumbnail,
playlist: playlist,
dontSkip: false,
}
return song, nil
}
@ -87,15 +85,15 @@ func (s *Song) Download() error {
// Plays the song. Once the song is playing, a notification is displayed in a text message that features the video thumbnail, URL, title,
// duration, and submitter.
func (s *Song) Play() {
if err := dj.audioStream.Play(fmt.Sprintf("%s/.mumbledj/songs/%s.m4a", dj.homeDir, s.youtubeId), dj.queue.OnItemFinished); err != nil {
if err := dj.audioStream.Play(fmt.Sprintf("%s/.mumbledj/songs/%s.m4a", dj.homeDir, s.youtubeId), dj.queue.OnSongFinished); err != nil {
panic(err)
} else {
if s.playlistTitle == "" {
if s.playlist == nil {
dj.client.Self.Channel.Send(fmt.Sprintf(NOW_PLAYING_HTML, s.thumbnailUrl, s.youtubeId, s.title,
s.duration, s.submitter), false)
} else {
dj.client.Self.Channel.Send(fmt.Sprintf(NOW_PLAYING_PLAYLIST_HTML, s.thumbnailUrl, s.youtubeId,
s.title, s.duration, s.submitter, s.playlistTitle), false)
s.title, s.duration, s.submitter, s.playlist.title), false)
}
}
}
@ -148,8 +146,3 @@ func (s *Song) SkipReached(channelUsers int) bool {
return false
}
}
// Returns "song" as the item type. Used for differentiating Songs from Playlists.
func (s *Song) ItemType() string {
return "song"
}

View file

@ -11,64 +11,54 @@ import (
"errors"
)
// QueueItem type declaration. QueueItem is an interface that groups together Song and Playlist
// types in a queue.
type QueueItem interface {
AddSkip(string) error
RemoveSkip(string) error
SkipReached(int) bool
ItemType() string
}
// SongQueue type declaration. Serves as a wrapper around the queue structure defined in queue.go.
// SongQueue type declaration.
type SongQueue struct {
queue []QueueItem
queue []*Song
}
// Initializes a new queue and returns the new SongQueue.
func NewSongQueue() *SongQueue {
return &SongQueue{
queue: make([]QueueItem, 0),
queue: make([]*Song, 0),
}
}
// Adds an item to the SongQueue.
func (q *SongQueue) AddItem(i QueueItem) error {
// Adds a Song to the SongQueue.
func (q *SongQueue) AddSong(s *Song) error {
beforeLen := q.Len()
q.queue = append(q.queue, i)
q.queue = append(q.queue, s)
if len(q.queue) == beforeLen+1 {
return nil
} else {
return errors.New("Could not add QueueItem to the SongQueue.")
return errors.New("Could not add Song to the SongQueue.")
}
}
// Returns the current QueueItem.
func (q *SongQueue) CurrentItem() QueueItem {
// Returns the current Song.
func (q *SongQueue) CurrentSong() *Song {
return q.queue[0]
}
// Moves to the next item in SongQueue. NextItem() removes the first value in the queue.
func (q *SongQueue) NextItem() {
// Moves to the next Song in SongQueue. NextSong() removes the first Song in the queue.
func (q *SongQueue) NextSong() {
if q.CurrentSong().playlist != nil {
if s, err := q.PeekNext(); err == nil {
if q.CurrentSong().playlist.id != s.playlist.id {
q.CurrentSong().playlist.DeleteSkippers()
}
} else {
q.CurrentSong().playlist.DeleteSkippers()
}
}
q.queue = q.queue[1:]
}
// Peeks at the next Song and returns it.
func (q *SongQueue) PeekNext() (*Song, error) {
if q.Len() != 0 {
if q.CurrentItem().ItemType() == "playlist" {
return q.CurrentItem().(*Playlist).songs.queue[1].(*Song), nil
} else if q.Len() > 1 {
if q.queue[1].ItemType() == "playlist" {
return q.queue[1].(*Playlist).songs.queue[0].(*Song), nil
} else {
return q.queue[1].(*Song), nil
}
} else {
return nil, errors.New("There is no song coming up next.")
}
if q.Len() > 1 {
return q.queue[1], nil
} else {
return nil, errors.New("There are no items in the queue.")
return nil, errors.New("There isn't a Song coming up next.")
}
}
@ -78,77 +68,35 @@ func (q *SongQueue) Len() int {
}
// A traversal function for SongQueue. Allows a visit function to be passed in which performs
// the specified action on each queue item. Traverses all individual songs, and all songs
// within playlists.
func (q *SongQueue) Traverse(visit func(i int, item QueueItem)) {
for iQueue, queueItem := range q.queue {
if queueItem.ItemType() == "playlist" {
for iPlaylist, playlistItem := range q.queue[iQueue].(*Playlist).songs.queue {
visit(iPlaylist, playlistItem)
}
} else {
visit(iQueue, queueItem)
}
// the specified action on each queue item.
func (q *SongQueue) Traverse(visit func(i int, s *Song)) {
for sQueue, queueSong := range q.queue {
visit(sQueue, queueSong)
}
}
// OnItemFinished event. Deletes item that just finished playing, then queues the next item.
func (q *SongQueue) OnItemFinished() {
// OnSongFinished event. Deletes Song that just finished playing, then queues the next Song (if exists).
func (q *SongQueue) OnSongFinished() {
if q.Len() != 0 {
if q.CurrentItem().ItemType() == "playlist" {
if err := q.CurrentItem().(*Playlist).songs.CurrentItem().(*Song).Delete(); err == nil {
if q.CurrentItem().(*Playlist).skipped == true {
if q.Len() > 1 {
q.NextItem()
q.PrepareAndPlayNextItem()
} else {
q.queue = q.queue[:0]
}
} else if q.CurrentItem().(*Playlist).songs.Len() > 1 {
q.CurrentItem().(*Playlist).songs.NextItem()
q.PrepareAndPlayNextItem()
} else {
if q.Len() > 1 {
q.NextItem()
q.PrepareAndPlayNextItem()
} else {
q.queue = q.queue[:0]
}
}
} else {
panic(err)
}
if dj.queue.CurrentSong().dontSkip == true {
dj.queue.CurrentSong().dontSkip = false
q.PrepareAndPlayNextSong()
} else {
if err := q.CurrentItem().(*Song).Delete(); err == nil {
if q.Len() > 1 {
q.NextItem()
q.PrepareAndPlayNextItem()
} else {
q.queue = q.queue[:0]
}
} else {
panic(err)
q.NextSong()
if q.Len() != 0 {
q.PrepareAndPlayNextSong()
}
}
}
}
func (q *SongQueue) PrepareAndPlayNextItem() {
if q.Len() != 0 {
if q.CurrentItem().ItemType() == "playlist" {
if err := q.CurrentItem().(*Playlist).songs.CurrentItem().(*Song).Download(); err == nil {
q.CurrentItem().(*Playlist).songs.CurrentItem().(*Song).Play()
} else {
dj.client.Self.Channel.Send(AUDIO_FAIL_MSG, false)
q.OnItemFinished()
}
} else {
if err := q.CurrentItem().(*Song).Download(); err == nil {
q.CurrentItem().(*Song).Play()
} else {
dj.client.Self.Channel.Send(AUDIO_FAIL_MSG, false)
q.OnItemFinished()
}
}
// Prepares next song and plays it if the download succeeds. Otherwise the function will print an error message
// to the channel and skip to the next song.
func (q *SongQueue) PrepareAndPlayNextSong() {
if err := q.CurrentSong().Download(); err == nil {
q.CurrentSong().Play()
} else {
dj.client.Self.Channel.Send(AUDIO_FAIL_MSG, false)
q.OnSongFinished()
}
}