From 317de0200a05202eb5b96fd23048ed0b8e2f0be3 Mon Sep 17 00:00:00 2001 From: Nikola Jovicic Date: Mon, 8 Feb 2016 12:55:56 +0100 Subject: [PATCH] add a search command for youtube and soundcloud --- README.md | 1 + commands.go | 24 ++++++++++++++++++++++- config.gcfg | 8 ++++++++ parseconfig.go | 2 ++ service.go | 44 +++++++++++++++++++++++++++++++++++++++++++ service_soundcloud.go | 19 +++++++++++++++++++ service_youtube.go | 23 ++++++++++++++++++++++ strings.go | 4 ++++ youtube_dl.go | 3 +++ 9 files changed, 127 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 225030e..48f06a8 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Command | Description | Arguments | Admin | Example --------|-------------|-----------|-------|-------- **add** | Adds audio from a url to the song queue. If no songs are currently in the queue, the audio will begin playing immediately. Playlists may also be added using this command. The maximum amount of songs that can be added from a playlist is specified in `mumbledj.gcfg`. | youtube_video_url OR youtube_playlist_url OR soundcloud_track_url OR soundcloud_playlist_url | No | `!add https://www.youtube.com/watch?v=5xfEr2Oxdys` **addnext** | Adds audio from a url to the song queue after the current song. If no songs are currently in the queue, the audio will begin playing immediately. Playlists may also be added using this command. The maximum amount of songs that can be added from a playlist is specified in `mumbledj.gcfg`. | youtube_video_url OR youtube_playlist_url OR soundcloud_track_url OR soundcloud_playlist_url | Yes | `!addnext https://www.youtube.com/watch?v=5xfEr2Oxdys` +**search** | Searches for a query in the specific service and add the first Video/Music found to the queue, as long as it's playable according to the MaxSongDuration set in `mumbledj.gcfg`. | yt OR sc AND query | No | `!search yt nyan cat` **skip**| Submits a vote to skip the current song. Once the skip ratio target (specified in `mumbledj.gcfg`) is met, the song will be skipped and the next will start playing. Each user may only submit one skip per song. | None | No | `!skip` **skipplaylist** | Submits a vote to skip the current playlist. Once the skip ratio target (specified in `mumbledj.gcfg`) is met, the playlist will be skipped and the next song/playlist will start playing. Each user may only submit one skip per playlist. | None | No | `!skipplaylist` **forceskip** | An admin command that forces a song skip. | None | Yes | `!forceskip` diff --git a/commands.go b/commands.go index 544569d..60774da 100644 --- a/commands.go +++ b/commands.go @@ -41,6 +41,14 @@ func parseCommand(user *gumble.User, username, command string) { } else { dj.SendPrivateMessage(user, NO_PERMISSION_MSG) } + + // Search command + case dj.conf.Aliases.SearchAlias: + if dj.HasPermission(username, dj.conf.Permissions.AdminSearch) { + searchSong(user, argument) + } else { + dj.SendPrivateMessage(user, NO_PERMISSION_MSG) + } // Addnext command case dj.conf.Aliases.AddNextAlias: if dj.HasPermission(username, dj.conf.Permissions.AdminAddNext) { @@ -239,6 +247,21 @@ func addNext(user *gumble.User, url string) error { } } +// searchSong performs !addnext functionality. Checks input searchString for service, and adds +// the found song to the queue as the next song if the format matches. +func searchSong(user *gumble.User, searchString string) error { + if searchString == "" { + dj.SendPrivateMessage(user, NO_ARGUMENT_MSG) + return errors.New("NO_ARGUMENT") + } else { + err := FindServiceAndSearch(user, searchString) + if err != nil { + dj.SendPrivateMessage(user, err.Error()) + } + return err + } +} + // skip performs !skip functionality. Adds a skip to the skippers slice for the current song, and then // evaluates if a skip should be performed. Both skip and forceskip are implemented here. func skip(user *gumble.User, admin, playlistSkip bool) { @@ -514,4 +537,3 @@ func listSongs(user *gumble.User, value string) { func version(user *gumble.User) { dj.SendPrivateMessage(user, DJ_VERSION) } - diff --git a/config.gcfg b/config.gcfg index c210feb..cd978ae 100644 --- a/config.gcfg +++ b/config.gcfg @@ -78,6 +78,10 @@ HighestVolume = 0.8 # DEFAULT VALUE: "add" AddAlias = "add" +# Alias used for search command +# DEFAULT VALUE: "search" +SearchAlias = "search" + # Alias used for addnext command # DEFAULT VALUE: "addnext" AddNextAlias = "addnext" @@ -185,6 +189,10 @@ Admins = "Matt" # DEFAULT VALUE: false AdminAdd = false +# Make search an admin command? +# DEFAULT VALUE: false +AdminSearch = false + # Make addnext an admin command? # DEFAULT VALUE: true AdminAddNext = true diff --git a/parseconfig.go b/parseconfig.go index 7309add..ec2dfdd 100644 --- a/parseconfig.go +++ b/parseconfig.go @@ -39,6 +39,7 @@ type DjConfig struct { } Aliases struct { AddAlias string + SearchAlias string AddNextAlias string SkipAlias string SkipPlaylistAlias string @@ -66,6 +67,7 @@ type DjConfig struct { AdminsEnabled bool Admins []string AdminAdd bool + AdminSearch bool AdminAddNext bool AdminAddPlaylists bool AdminSkip bool diff --git a/service.go b/service.go index 02f9da5..318b3ec 100644 --- a/service.go +++ b/service.go @@ -12,6 +12,8 @@ import ( "fmt" "regexp" "time" + "net/url" + "strings" "github.com/layeh/gumble/gumble" ) @@ -21,7 +23,9 @@ type Service interface { ServiceName() string TrackName() string URLRegex(string) bool + SearchRegex(string) bool NewRequest(*gumble.User, string) ([]Song, error) + SearchSong(string) (string, error) } // Song interface. Each service will implement these @@ -130,6 +134,46 @@ func FindServiceAndAdd(user *gumble.User, url string) error { } } +func FindServiceAndSearch(user *gumble.User, searchString string) error { + var searchService Service + + var serviceProvider, argument string + split := strings.Split(searchString, "\n") + splitString := split[0] + if strings.Contains(splitString, " ") { + index := strings.Index(splitString, " ") + serviceProvider, argument = splitString[0:index], splitString[(index+1):] + argument = url.QueryEscape(argument) + } else { + return errors.New("NO_ARGUMENT") + } + + // Checks all services to see if any can take the URL + for _, service := range services { + if service.SearchRegex(serviceProvider) { + searchService = service + } + } + + if searchService == nil { + return errors.New(INVALID_SEARCH_PROVIDER) + } else { + var songURL string + var err error + + // Get service to create songs + if songURL, err = searchService.SearchSong(argument); err != nil { + return err + } + + if err = FindServiceAndAdd(user, songURL); err != nil { + return err + } + + return nil + } +} + // FindServiceAndInsertNext tries the given url with each service // and inserts the song/playlist with the correct service into the slot after the current one func FindServiceAndInsertNext(user *gumble.User, url string) error { diff --git a/service_soundcloud.go b/service_soundcloud.go index 7a11914..de29f57 100644 --- a/service_soundcloud.go +++ b/service_soundcloud.go @@ -21,6 +21,9 @@ import ( var soundcloudSongPattern = `https?:\/\/(www\.)?soundcloud\.com\/([\w-]+)\/([\w-]+)(#t=\n\n?(:\n\n)*)?` var soundcloudPlaylistPattern = `https?:\/\/(www\.)?soundcloud\.com\/([\w-]+)\/sets\/([\w-]+)` +// SearchService name +var soundcloudSearchServiceName = "sc" + // SoundCloud implements the Service interface type SoundCloud struct{} @@ -43,6 +46,10 @@ func (sc SoundCloud) URLRegex(url string) bool { return RegexpFromURL(url, []string{soundcloudSongPattern, soundcloudPlaylistPattern}) != nil } +func (sc SoundCloud) SearchRegex(searchService string) bool { + return searchService == soundcloudSearchServiceName +} + // NewRequest creates the requested song/playlist and adds to the queue func (sc SoundCloud) NewRequest(user *gumble.User, url string) ([]Song, error) { var apiResponse *jsonq.JsonQuery @@ -97,6 +104,18 @@ func (sc SoundCloud) NewRequest(user *gumble.User, url string) ([]Song, error) { } } +// SearchSong searches for a Song and adds the first hit +func (sc SoundCloud) SearchSong(searchString string) (string, error) { + var returnString string + url := fmt.Sprintf("https://api.soundcloud.com/tracks?q=%s&client_id=%s&limit=1", searchString, dj.conf.ServiceKeys.SoundCloud) + + if apiResponse, err := PerformGetRequest(url); err == nil { + returnString, _ = apiResponse.String("json", "0", "permalink_url"); + return returnString, nil + } + return "", errors.New(fmt.Sprintf(INVALID_API_KEY, sc.ServiceName())) +} + // NewSong creates a track and adds to the queue func (sc SoundCloud) NewSong(user *gumble.User, trackData *jsonq.JsonQuery, offset int, playlist Playlist) (Song, error) { title, _ := trackData.String("title") diff --git a/service_youtube.go b/service_youtube.go index 268ec73..73e9015 100644 --- a/service_youtube.go +++ b/service_youtube.go @@ -30,6 +30,12 @@ var youtubeVideoPatterns = []string{ `https?:\/\/www.youtube.com\/v\/([\w-]+)(\?t=\d*m?\d*s?)?`, } +// SearchService name +var youtubeSearchServiceName = "yt" + +// SearchService vidoe UTL prefix +var videoURLprefix = "https://www.youtube.com/watch?v=" + // YouTube implements the Service interface type YouTube struct{} @@ -48,6 +54,10 @@ func (yt YouTube) URLRegex(url string) bool { return RegexpFromURL(url, append(youtubeVideoPatterns, []string{youtubePlaylistPattern}...)) != nil } +func (yt YouTube) SearchRegex(searchService string) bool { + return searchService == youtubeSearchServiceName +} + // NewRequest creates the requested song/playlist and adds to the queue func (yt YouTube) NewRequest(user *gumble.User, url string) ([]Song, error) { var songArray []Song @@ -75,6 +85,19 @@ func (yt YouTube) NewRequest(user *gumble.User, url string) ([]Song, error) { } } +// SearchSong searches for a Song and adds the first hit +func (yt YouTube) SearchSong(searchString string) (string, error) { + var returnString, apiStringValue string + searchURL := fmt.Sprintf("https://www.googleapis.com/youtube/v3/search?part=snippet&q=%s&key=%s&maxResults=1&type=video", searchString, dj.conf.ServiceKeys.Youtube) + + if apiResponse, err := PerformGetRequest(searchURL); err == nil { + apiStringValue, _ = apiResponse.String("items", "0", "id", "videoId") + returnString = videoURLprefix + apiStringValue + return returnString, nil + } + return "", errors.New(fmt.Sprintf(INVALID_API_KEY, yt.ServiceName())) +} + // NewSong gathers the metadata for a song extracted from a YouTube video, and returns the song. func (yt YouTube) NewSong(user *gumble.User, id, offset string, playlist Playlist) (Song, error) { url := fmt.Sprintf("https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails&id=%s&key=%s", id, dj.conf.ServiceKeys.Youtube) diff --git a/strings.go b/strings.go index 0d6ea5b..20cf8d4 100644 --- a/strings.go +++ b/strings.go @@ -31,6 +31,9 @@ const CHANNEL_DOES_NOT_EXIST_MSG = "The channel you specified does not exist." // Message shown to users when they attempt to add an invalid URL to the queue. const INVALID_URL_MSG = "The URL you submitted does not match the required format." +// Message shown to users when they attempt to search on an invalid platform. +const INVALID_SEARCH_PROVIDER = "The Search provider you submitted does not match the required format." + // Message shown to users when they attempt to add a video that's too long const TRACK_TOO_LONG_MSG = "The %s you submitted exceeds the duration allowed by the server." @@ -129,6 +132,7 @@ const PLAYLIST_SKIPPED_HTML = ` const HELP_HTML = `
User Commands:

!help - Displays this help.

+

!search (yt|sc) query - Search on Youtube or Soundcloud for a query and add first hit.

!add - Adds songs/playlists to queue.

!volume - Either tells you the current volume or sets it to a new volume.

!skip - Casts a vote to skip the current song

diff --git a/youtube_dl.go b/youtube_dl.go index ff39510..e3c3840 100644 --- a/youtube_dl.go +++ b/youtube_dl.go @@ -257,6 +257,9 @@ func PerformGetRequest(url string) (*jsonq.JsonQuery, error) { if response.StatusCode == 200 { if body, err := ioutil.ReadAll(response.Body); err == nil { jsonString = string(body) + if jsonString[0] == '[' { + jsonString = "{\"json\":" + jsonString + "}" + } } } else { if response.StatusCode == 403 {