diff --git a/service.go b/service.go index 13a1d08..a83ddba 100644 --- a/service.go +++ b/service.go @@ -52,7 +52,7 @@ type Playlist interface { Title() string } -var services = []Service{YouTube{}} +var services = []Service{YouTube{}, SoundCloud{}} func findServiceAndAdd(user *gumble.User, url string) error { var urlService Service diff --git a/service_soundcloud.go b/service_soundcloud.go index 0d63bc4..01def80 100644 --- a/service_soundcloud.go +++ b/service_soundcloud.go @@ -13,19 +13,9 @@ import ( var soundcloudSongPattern = `https?:\/\/(www)?\.soundcloud\.com\/([\w-]+)\/([\w-]+)` var soundcloudPlaylistPattern = `https?:\/\/(www)?\.soundcloud\.com\/([\w-]+)\/sets\/([\w-]+)` -// ------ -// TYPES -// ------ - -// YouTube implements the Service interface +// SoundCloud implements the Service interface type SoundCloud struct{} -// YouTubePlaylist holds the metadata for a YouTube playlist. -type SoundCloudPlaylist struct { - id string - title string -} - // ------------------ // SOUNDCLOUD SERVICE // ------------------ @@ -59,7 +49,7 @@ func (sc SoundCloud) NewRequest(user *gumble.User, url string) (string, error) { // Create playlist title, _ := apiResponse.String("title") permalink, _ := apiResponse.String("permalink_url") - playlist := &SoundCloudPlaylist{ + playlist := &YouTubeDLPlaylist{ id: permalink, title: title, } @@ -73,6 +63,7 @@ func (sc SoundCloud) NewRequest(user *gumble.User, url string) (string, error) { return "", errors.New("NO_PLAYLIST_PERMISSION") } } else { + // SONG return sc.NewSong(user.Name, apiResponse, nil) } } @@ -96,7 +87,7 @@ func (sc SoundCloud) NewSong(user string, trackData *jsonq.JsonQuery, playlist P return "", err } - song := &YouTubeDL{ + song := &YouTubeDLSong{ id: id, title: title, thumbnail: thumbnail, @@ -107,57 +98,5 @@ func (sc SoundCloud) NewSong(user string, trackData *jsonq.JsonQuery, playlist P 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 *SoundCloudPlaylist) 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 *SoundCloudPlaylist) 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 *SoundCloudPlaylist) 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 *SoundCloudPlaylist) ID() string { - return p.id -} - -// Title returns the title of the YouTubePlaylist. -func (p *SoundCloudPlaylist) Title() string { - return p.title + return title, nil } diff --git a/service_youtube.go b/service_youtube.go index 5da5607..4e457d9 100644 --- a/service_youtube.go +++ b/service_youtube.go @@ -42,26 +42,6 @@ var youtubeVideoPatterns = []string{ // YouTube implements the Service interface type YouTube struct{} -// YouTubeSong holds the metadata for a song extracted from a YouTube video. -type YouTubeSong 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 YouTubePlaylist struct { - id string - title string -} - // --------------- // YOUTUBE SERVICE // --------------- @@ -105,7 +85,7 @@ func (yt YouTube) NewRequest(user *gumble.User, url string) (string, error) { // NewSong gathers the metadata for a song extracted from a YouTube video, and returns // the song. -func (yt YouTube) NewSong(user, id, offset string, playlist *YouTubePlaylist) (*YouTubeSong, error) { +func (yt YouTube) NewSong(user, id, offset string, playlist *Playlist) (*Song, error) { var apiResponse *jsonq.JsonQuery var err error url := fmt.Sprintf("https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails&id=%s&key=%s", @@ -179,7 +159,7 @@ func (yt YouTube) NewSong(user, id, offset string, playlist *YouTubePlaylist) (* } if dj.conf.General.MaxSongDuration == 0 || totalSeconds <= dj.conf.General.MaxSongDuration { - song := &YouTubeSong{ + song := &YouTubeDLSong{ submitter: user, title: title, id: id, @@ -200,7 +180,7 @@ func (yt YouTube) NewSong(user, id, offset string, playlist *YouTubePlaylist) (* } // NewPlaylist gathers the metadata for a YouTube playlist and returns it. -func (yt YouTube) NewPlaylist(user, id string) (*YouTubePlaylist, error) { +func (yt YouTube) NewPlaylist(user, id string) (*Playlist, error) { var apiResponse *jsonq.JsonQuery var err error // Retrieve title of playlist @@ -211,7 +191,7 @@ func (yt YouTube) NewPlaylist(user, id string) (*YouTubePlaylist, error) { } title, _ := apiResponse.String("items", "0", "snippet", "title") - playlist := &YouTubePlaylist{ + playlist := &YouTubeDLPlaylist{ id: id, title: title, } @@ -235,239 +215,6 @@ func (yt YouTube) NewPlaylist(user, id string) (*YouTubePlaylist, error) { return playlist, nil } -// ------------ -// YOUTUBE SONG -// ------------ - -// 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 (s *YouTubeSong) Download() error { - - // Checks to see if song is already downloaded - if _, err := os.Stat(fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, s.Filename())); os.IsNotExist(err) { - Verbose("Downloading " + s.Title()) - cmd := exec.Command("youtube-dl", "--output", fmt.Sprintf(`~/.mumbledj/songs/%s`, s.Filename()), "--format", "m4a", "--", s.ID()) - if err := cmd.Run(); err == nil { - if dj.conf.Cache.Enabled { - dj.cache.CheckMaximumDirectorySize() - } - Verbose(s.Title() + " downloaded") - return nil - } - Verbose(s.Title() + " failed to download") - 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 video -// thumbnail, URL, title, duration, and submitter. -func (s *YouTubeSong) Play() { - if s.offset != 0 { - offsetDuration, _ := time.ParseDuration(fmt.Sprintf("%ds", s.offset)) - dj.audioStream.Offset = offsetDuration - } - dj.audioStream.Source = gumble_ffmpeg.SourceFile(fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, s.Filename())) - if err := dj.audioStream.Play(); err != nil { - panic(err) - } else { - if isNil(s.Playlist()) { - message := ` - - - - - - - - - - -
%s (%s)
Added by %s
- ` - dj.client.Self.Channel.Send(fmt.Sprintf(message, s.Thumbnail(), s.ID(), s.Title(), - s.Duration(), s.Submitter()), false) - } else { - message := ` - - - - - - - - - - - - - -
%s (%s)
Added by %s
From playlist "%s"
- ` - dj.client.Self.Channel.Send(fmt.Sprintf(message, s.Thumbnail(), s.ID(), - s.Title(), s.Duration(), s.Submitter(), s.Playlist().Title()), false) - } - Verbose("Now playing " + s.Title()) - - go func() { - dj.audioStream.Wait() - dj.queue.OnSongFinished() - }() - } -} - -// Delete deletes the song from ~/.mumbledj/songs if the cache is disabled. -func (s *YouTubeSong) Delete() error { - if dj.conf.Cache.Enabled == false { - filePath := fmt.Sprintf("%s/.mumbledj/songs/%s.m4a", dj.homeDir, s.ID()) - if _, err := os.Stat(filePath); err == nil { - if err := os.Remove(filePath); err == nil { - Verbose("Deleted " + s.Title()) - return nil - } - Verbose("Failed to delete " + s.Title()) - 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 (s *YouTubeSong) AddSkip(username string) error { - for _, user := range s.skippers { - if username == user { - return errors.New("This user has already skipped the current song.") - } - } - s.skippers = append(s.skippers, username) - return nil -} - -// RemoveSkip removes a skip from the skippers slice. If username is not in slice, an error is -// returned. -func (s *YouTubeSong) RemoveSkip(username string) error { - for i, user := range s.skippers { - if username == user { - s.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 (s *YouTubeSong) SkipReached(channelUsers int) bool { - if float32(len(s.skippers))/float32(channelUsers) >= dj.conf.General.SkipRatio { - return true - } - return false -} - -// Submitter returns the name of the submitter of the YouTubeSong. -func (s *YouTubeSong) Submitter() string { - return s.submitter -} - -// Title returns the title of the YouTubeSong. -func (s *YouTubeSong) Title() string { - return s.title -} - -// ID returns the id of the YouTubeSong. -func (s *YouTubeSong) ID() string { - return s.id -} - -// Filename returns the filename of the YouTubeSong. -func (s *YouTubeSong) Filename() string { - return s.filename -} - -// Duration returns the duration of the YouTubeSong. -func (s *YouTubeSong) Duration() string { - return s.duration -} - -// Thumbnail returns the thumbnail URL for the YouTubeSong. -func (s *YouTubeSong) Thumbnail() string { - return s.thumbnail -} - -// Playlist returns the playlist type for the YouTubeSong (may be nil). -func (s *YouTubeSong) Playlist() Playlist { - return s.playlist -} - -// DontSkip returns the DontSkip boolean value for the YouTubeSong. -func (s *YouTubeSong) DontSkip() bool { - return s.dontSkip -} - -// SetDontSkip sets the DontSkip boolean value for the YouTubeSong. -func (s *YouTubeSong) SetDontSkip(value bool) { - s.dontSkip = value -} - -// ---------------- -// YOUTUBE PLAYLIST -// ---------------- - -// AddSkip adds a skip to the playlist's skippers slice. -func (p *YouTubePlaylist) 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 -} - -// ----------- -// YOUTUBE API -// ----------- - // PerformGetRequest does all the grunt work for a YouTube HTTPS GET request. func PerformGetRequest(url string) (*jsonq.JsonQuery, error) { jsonString := "" diff --git a/youtube_dl.go b/youtube_dl.go index 5e713cb..b7afff2 100644 --- a/youtube_dl.go +++ b/youtube_dl.go @@ -10,7 +10,8 @@ import ( "github.com/layeh/gumble/gumble_ffmpeg" ) -type YouTubeDL struct { +// Extends a Song +type YouTubeDLSong struct { id string title string thumbnail string @@ -23,9 +24,18 @@ type YouTubeDL struct { dontSkip bool } +type YouTubeDLPlaylist struct { + id string + title string +} + +// ------------- +// YouTubeDLSong +// ------------- + // 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 { +func (dl *YouTubeDLSong) Download() error { // Checks to see if song is already downloaded if _, err := os.Stat(fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, dl.id+".m4a")); os.IsNotExist(err) { @@ -43,7 +53,7 @@ func (dl *YouTubeDL) Download() error { // 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() { +func (dl *YouTubeDLSong) Play() { if dl.offset != 0 { offsetDuration, _ := time.ParseDuration(fmt.Sprintf("%ds", dl.offset)) dj.audioStream.Offset = offsetDuration @@ -70,7 +80,7 @@ func (dl *YouTubeDL) Play() { } // Delete deletes the song from ~/.mumbledj/songs if the cache is disabled. -func (dl *YouTubeDL) Delete() error { +func (dl *YouTubeDLSong) 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 { @@ -86,7 +96,7 @@ func (dl *YouTubeDL) Delete() error { // 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 { +func (dl *YouTubeDLSong) AddSkip(username string) error { for _, user := range dl.skippers { if username == user { return errors.New("This user has already skipped the current song.") @@ -98,7 +108,7 @@ func (dl *YouTubeDL) AddSkip(username string) error { // 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 { +func (dl *YouTubeDLSong) RemoveSkip(username string) error { for i, user := range dl.skippers { if username == user { dl.skippers = append(dl.skippers[:i], dl.skippers[i+1:]...) @@ -111,7 +121,7 @@ func (dl *YouTubeDL) RemoveSkip(username string) error { // 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 { +func (dl *YouTubeDLSong) SkipReached(channelUsers int) bool { if float32(len(dl.skippers))/float32(channelUsers) >= dj.conf.General.SkipRatio { return true } @@ -119,46 +129,98 @@ func (dl *YouTubeDL) SkipReached(channelUsers int) bool { } // Submitter returns the name of the submitter of the Song. -func (dl *YouTubeDL) Submitter() string { +func (dl *YouTubeDLSong) Submitter() string { return dl.submitter } // Title returns the title of the Song. -func (dl *YouTubeDL) Title() string { +func (dl *YouTubeDLSong) Title() string { return dl.title } // ID returns the id of the Song. -func (dl *YouTubeDL) ID() string { +func (dl *YouTubeDLSong) ID() string { return dl.id } // Filename returns the filename of the Song. -func (dl *YouTubeDL) Filename() string { +func (dl *YouTubeDLSong) Filename() string { return dl.id + ".m4a" } // Duration returns the duration of the Song. -func (dl *YouTubeDL) Duration() string { +func (dl *YouTubeDLSong) Duration() string { return dl.duration } // Thumbnail returns the thumbnail URL for the Song. -func (dl *YouTubeDL) Thumbnail() string { +func (dl *YouTubeDLSong) Thumbnail() string { return dl.thumbnail } // Playlist returns the playlist type for the Song (may be nil). -func (dl *YouTubeDL) Playlist() Playlist { +func (dl *YouTubeDLSong) Playlist() Playlist { return dl.playlist } // DontSkip returns the DontSkip boolean value for the Song. -func (dl *YouTubeDL) DontSkip() bool { +func (dl *YouTubeDLSong) DontSkip() bool { return dl.dontSkip } // SetDontSkip sets the DontSkip boolean value for the Song. -func (dl *YouTubeDL) SetDontSkip(value bool) { +func (dl *YouTubeDLSong) SetDontSkip(value bool) { dl.dontSkip = value } + +// ---------------- +// YOUTUBE PLAYLIST +// ---------------- + +// AddSkip adds a skip to the playlist's skippers slice. +func (p *YouTubeDLPlaylist) 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 *YouTubeDLPlaylist) 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 *YouTubeDLPlaylist) 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 *YouTubeDLPlaylist) 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 YouTubeDLPlaylist. +func (p *YouTubeDLPlaylist) ID() string { + return p.id +} + +// Title returns the title of the YouTubeDLPlaylist. +func (p *YouTubeDLPlaylist) Title() string { + return p.title +}