add a search command for youtube and soundcloud

This commit is contained in:
Nikola Jovicic 2016-02-08 12:55:56 +01:00
parent aa285bf817
commit 317de0200a
9 changed files with 127 additions and 1 deletions

View file

@ -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`

View file

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

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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")

View file

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

View file

@ -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 = `<br/>
<b>User Commands:</b>
<p><b>!help</b> - Displays this help.</p>
<p><b>!search (yt|sc) query</b> - Search on Youtube or Soundcloud for a query and add first hit.</p>
<p><b>!add</b> - Adds songs/playlists to queue.</p>
<p><b>!volume</b> - Either tells you the current volume or sets it to a new volume.</p>
<p><b>!skip</b> - Casts a vote to skip the current song</p>

View file

@ -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 {