diff --git a/service_soundcloud.go b/service_soundcloud.go index 34944e7..fed2a3b 100644 --- a/service_soundcloud.go +++ b/service_soundcloud.go @@ -29,20 +29,6 @@ var soundcloudPlaylistPattern = `https?:\/\/(www)?\.soundcloud\.com\/([\w-]+)\/s // YouTube implements the Service interface type SoundCloud struct{} -// YouTubeSong holds the metadata for a song extracted from a YouTube video. -type SoundCloudSong struct { - submitter string - title string - id string - offset int - filename string - duration string - thumbnail string - skippers []string - playlist Playlist - dontSkip bool -} - // YouTubePlaylist holds the metadata for a YouTube playlist. type SoundCloudPlaylist struct { id string @@ -72,25 +58,115 @@ func (sc SoundCloud) NewRequest(user *gumble.User, url string) (string, error) { return nil, errors.New(INVALID_API_KEY) } - title, _ := apiResponse.String("title") tracks, err := apiResponse.ArrayOfObjects("tracks") - if err == nil { - if re.MatchString(url) { - // PLAYLIST - if dj.HasPermission(user.Name, dj.conf.Permissions.AdminAddPlaylists) { - playlist, err := sc.NewPlaylist(user.Name, url) - return playlist.Title(), err - } else { - return "", errors.New("NO_PLAYLIST_PERMISSION") - } - } else { + // PLAYLIST + if dj.HasPermission(user.Name, dj.conf.Permissions.AdminAddPlaylists) { + // Check duration of playlist + // duration, _ := apiResponse.Int("duration") - // SONG - song, err := sc.NewSong(user.Name, url, nil) - return song.Title(), err + // Create playlist + title, _ := apiResponse.String("title") + permalink, _ := apiResponse.String("permalink_url") + playlist := &SoundCloudPlaylist{ + id: permalink, + title: title, + } + + // Add all tracks + for _, t := range tracks { + sc.NewSong(user.Name, jsonq.NewQuery(t), playlist) + } + return playlist.Title(), err + } else { + return "", errors.New("NO_PLAYLIST_PERMISSION") } } else { - return "", err + return sc.NewSong(user.Name, apiResponse, nil) } } + +// Creates a track and adds to the queue +func (sc SoundCloud) NewSong(user string, trackData *jsonq.JsonQuery, playlist SoundCloudPlaylist) (string, error) { + title, err := trackData.String("title") + if err != nil { + return "", err + } + id, err := trackData.String("id") + if err != nil { + return "", err + } + duration, err := trackData.Int("duration") + if err != nil { + return "", err + } + thumbnail, err := trackData.String("artwork_uri") + if err != nil { + return "", err + } + + song := &YoutubeDL{ + id: id, + title: title, + thumbnail: thumbnail, + submitter: user.Name, + duration: duration, + playlist: playlist, + skippers: make([]string, 0), + dontSkip: false, + } + dj.queue.AddSong(song) + return title, nil +} + +// ---------------- +// YOUTUBE PLAYLIST +// ---------------- + +// AddSkip adds a skip to the playlist's skippers slice. +func (p *SoundCloudPlaylist) AddSkip(username string) error { + for _, user := range dj.playlistSkips[p.ID()] { + if username == user { + return errors.New("This user has already skipped the current song.") + } + } + dj.playlistSkips[p.ID()] = append(dj.playlistSkips[p.ID()], username) + return nil +} + +// RemoveSkip removes a skip from the playlist's skippers slice. If username is not in the slice +// an error is returned. +func (p *YouTubePlaylist) RemoveSkip(username string) error { + for i, user := range dj.playlistSkips[p.ID()] { + if username == user { + 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.") +} + +// DeleteSkippers removes the skippers entry in dj.playlistSkips. +func (p *YouTubePlaylist) DeleteSkippers() { + delete(dj.playlistSkips, p.ID()) +} + +// SkipReached calculates the current skip ratio based on the number of users within MumbleDJ's +// channel and the number of usernames in the skippers slice. If the value is greater than or equal +// to the skip ratio defined in the config, the function returns true, and returns false otherwise. +func (p *YouTubePlaylist) SkipReached(channelUsers int) bool { + if float32(len(dj.playlistSkips[p.ID()]))/float32(channelUsers) >= dj.conf.General.PlaylistSkipRatio { + return true + } + return false +} + +// ID returns the id of the YouTubePlaylist. +func (p *YouTubePlaylist) ID() string { + return p.id +} + +// Title returns the title of the YouTubePlaylist. +func (p *YouTubePlaylist) Title() string { + return p.title +} diff --git a/youtube_dl.go b/youtube_dl.go new file mode 100644 index 0000000..d830eab --- /dev/null +++ b/youtube_dl.go @@ -0,0 +1,154 @@ +package main + +import () + +type YouTubeDL struct { + id string + title string + thumbnail string + submitter string + duration string + playlist Playlist + skippers []string + dontSkip bool +} + +// Download downloads the song via youtube-dl if it does not already exist on disk. +// All downloaded songs are stored in ~/.mumbledj/songs and should be automatically cleaned. +func (dl *YouTubeDL) Download() error { + + // Checks to see if song is already downloaded + if _, err := os.Stat(fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, id+".m4a")); os.IsNotExist(err) { + cmd := exec.Command("youtube-dl", "--output", fmt.Sprintf(`~/.mumbledj/songs/%s`, id+".m4a"), "--format", "m4a", "--", url) + if err := cmd.Run(); err == nil { + if dj.conf.Cache.Enabled { + dj.cache.CheckMaximumDirectorySize() + } + return nil + } + return errors.New("Song download failed.") + } + return nil +} + +// Play plays the song. Once the song is playing, a notification is displayed in a text message that features the song +// thumbnail, URL, title, duration, and submitter. +func (dl *YouTubeDL) Play() { + if s.offset != 0 { + offsetDuration, _ := time.ParseDuration(fmt.Sprintf("%ds", dl.offset)) + dj.audioStream.Offset = offsetDuration + } + dj.audioStream.Source = gumble_ffmpeg.SourceFile(fmt.Sprintf("%s/.mumbledj/songs/%s.m4a", dj.homeDir, dl.id)) + if err := dj.audioStream.Play(); err != nil { + panic(err) + } else { + message := `` + message = fmt.Sprintf(message, dl.thumbnail, dl.url, dl.title, dl.duration, dl.submitter) + if isNil(dl.playlist) { + dj.client.Self.Channel.Send(message+`
%s (%s)
Added by %s
`, false) + } else { + message += `From playlist "%s"` + dj.client.Self.Channel.Send(fmt.Sprintf(message, dl.playlist.Title()), false) + } + Verbose("Now playing " + dl.title) + + go func() { + dj.audioStream.Wait() + dj.queue.OnSongFinished() + }() + } +} + +// Delete deletes the song from ~/.mumbledj/songs if the cache is disabled. +func (dl *YouTubeDL) Delete() error { + if dj.conf.Cache.Enabled == false { + filePath := fmt.Sprintf("%s/.mumbledj/songs/%s.m4a", dj.homeDir, dl.id) + if _, err := os.Stat(filePath); err == nil { + if err := os.Remove(filePath); err == nil { + return nil + } + return errors.New("Error occurred while deleting audio file.") + } + return nil + } + return nil +} + +// AddSkip 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. +func (dl *YouTubeDL) AddSkip(username string) error { + for _, user := range dl.skippers { + if username == user { + return errors.New("This user has already skipped the current song.") + } + } + dl.skippers = append(dl.skippers, username) + return nil +} + +// RemoveSkip removes a skip from the skippers slice. If username is not in slice, an error is +// returned. +func (dl *YouTubeDL) RemoveSkip(username string) error { + for i, user := range dl.skippers { + if username == user { + dl.skippers = append(s.skippers[:i], s.skippers[i+1:]...) + return nil + } + } + return errors.New("This user has not skipped the song.") +} + +// SkipReached calculates the current skip ratio based on the number of users within MumbleDJ's +// channel and the number of usernames in the skippers slice. If the value is greater than or equal +// to the skip ratio defined in the config, the function returns true, and returns false otherwise. +func (dl *YouTubeDL) SkipReached(channelUsers int) bool { + if float32(len(dl.skippers))/float32(channelUsers) >= dj.conf.General.SkipRatio { + return true + } + return false +} + +// Submitter returns the name of the submitter of the Song. +func (dl *YouTubeDL) Submitter() string { + return dl.submitter +} + +// Title returns the title of the Song. +func (dl *YouTubeDL) Title() string { + return dl.title +} + +// ID returns the id of the Song. +func (dl *YouTubeDL) ID() string { + return dl.id +} + +// Filename returns the filename of the Song. +func (dl *YouTubeDL) Filename() string { + return dl.id + ".m4a" +} + +// Duration returns the duration of the Song. +func (dl *YouTubeDL) Duration() string { + return dl.duration +} + +// Thumbnail returns the thumbnail URL for the Song. +func (dl *YouTubeDL) Thumbnail() string { + return dl.thumbnail +} + +// Playlist returns the playlist type for the Song (may be nil). +func (dl *YouTubeDL) Playlist() Playlist { + return dl.playlist +} + +// DontSkip returns the DontSkip boolean value for the Song. +func (dl *YouTubeDL) DontSkip() bool { + return dl.dontSkip +} + +// SetDontSkip sets the DontSkip boolean value for the Song. +func (dl *YouTubeDL) SetDontSkip(value bool) { + dl.dontSkip = value +}