add a search command for youtube and soundcloud
This commit is contained in:
parent
aa285bf817
commit
317de0200a
|
@ -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`
|
**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`
|
**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`
|
**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`
|
**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`
|
**forceskip** | An admin command that forces a song skip. | None | Yes | `!forceskip`
|
||||||
|
|
24
commands.go
24
commands.go
|
@ -41,6 +41,14 @@ func parseCommand(user *gumble.User, username, command string) {
|
||||||
} else {
|
} else {
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
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
|
// Addnext command
|
||||||
case dj.conf.Aliases.AddNextAlias:
|
case dj.conf.Aliases.AddNextAlias:
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminAddNext) {
|
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
|
// 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.
|
// evaluates if a skip should be performed. Both skip and forceskip are implemented here.
|
||||||
func skip(user *gumble.User, admin, playlistSkip bool) {
|
func skip(user *gumble.User, admin, playlistSkip bool) {
|
||||||
|
@ -514,4 +537,3 @@ func listSongs(user *gumble.User, value string) {
|
||||||
func version(user *gumble.User) {
|
func version(user *gumble.User) {
|
||||||
dj.SendPrivateMessage(user, DJ_VERSION)
|
dj.SendPrivateMessage(user, DJ_VERSION)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -78,6 +78,10 @@ HighestVolume = 0.8
|
||||||
# DEFAULT VALUE: "add"
|
# DEFAULT VALUE: "add"
|
||||||
AddAlias = "add"
|
AddAlias = "add"
|
||||||
|
|
||||||
|
# Alias used for search command
|
||||||
|
# DEFAULT VALUE: "search"
|
||||||
|
SearchAlias = "search"
|
||||||
|
|
||||||
# Alias used for addnext command
|
# Alias used for addnext command
|
||||||
# DEFAULT VALUE: "addnext"
|
# DEFAULT VALUE: "addnext"
|
||||||
AddNextAlias = "addnext"
|
AddNextAlias = "addnext"
|
||||||
|
@ -185,6 +189,10 @@ Admins = "Matt"
|
||||||
# DEFAULT VALUE: false
|
# DEFAULT VALUE: false
|
||||||
AdminAdd = false
|
AdminAdd = false
|
||||||
|
|
||||||
|
# Make search an admin command?
|
||||||
|
# DEFAULT VALUE: false
|
||||||
|
AdminSearch = false
|
||||||
|
|
||||||
# Make addnext an admin command?
|
# Make addnext an admin command?
|
||||||
# DEFAULT VALUE: true
|
# DEFAULT VALUE: true
|
||||||
AdminAddNext = true
|
AdminAddNext = true
|
||||||
|
|
|
@ -39,6 +39,7 @@ type DjConfig struct {
|
||||||
}
|
}
|
||||||
Aliases struct {
|
Aliases struct {
|
||||||
AddAlias string
|
AddAlias string
|
||||||
|
SearchAlias string
|
||||||
AddNextAlias string
|
AddNextAlias string
|
||||||
SkipAlias string
|
SkipAlias string
|
||||||
SkipPlaylistAlias string
|
SkipPlaylistAlias string
|
||||||
|
@ -66,6 +67,7 @@ type DjConfig struct {
|
||||||
AdminsEnabled bool
|
AdminsEnabled bool
|
||||||
Admins []string
|
Admins []string
|
||||||
AdminAdd bool
|
AdminAdd bool
|
||||||
|
AdminSearch bool
|
||||||
AdminAddNext bool
|
AdminAddNext bool
|
||||||
AdminAddPlaylists bool
|
AdminAddPlaylists bool
|
||||||
AdminSkip bool
|
AdminSkip bool
|
||||||
|
|
44
service.go
44
service.go
|
@ -12,6 +12,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/layeh/gumble/gumble"
|
"github.com/layeh/gumble/gumble"
|
||||||
)
|
)
|
||||||
|
@ -21,7 +23,9 @@ type Service interface {
|
||||||
ServiceName() string
|
ServiceName() string
|
||||||
TrackName() string
|
TrackName() string
|
||||||
URLRegex(string) bool
|
URLRegex(string) bool
|
||||||
|
SearchRegex(string) bool
|
||||||
NewRequest(*gumble.User, string) ([]Song, error)
|
NewRequest(*gumble.User, string) ([]Song, error)
|
||||||
|
SearchSong(string) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Song interface. Each service will implement these
|
// 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
|
// FindServiceAndInsertNext tries the given url with each service
|
||||||
// and inserts the song/playlist with the correct service into the slot after the current one
|
// and inserts the song/playlist with the correct service into the slot after the current one
|
||||||
func FindServiceAndInsertNext(user *gumble.User, url string) error {
|
func FindServiceAndInsertNext(user *gumble.User, url string) error {
|
||||||
|
|
|
@ -21,6 +21,9 @@ import (
|
||||||
var soundcloudSongPattern = `https?:\/\/(www\.)?soundcloud\.com\/([\w-]+)\/([\w-]+)(#t=\n\n?(:\n\n)*)?`
|
var soundcloudSongPattern = `https?:\/\/(www\.)?soundcloud\.com\/([\w-]+)\/([\w-]+)(#t=\n\n?(:\n\n)*)?`
|
||||||
var soundcloudPlaylistPattern = `https?:\/\/(www\.)?soundcloud\.com\/([\w-]+)\/sets\/([\w-]+)`
|
var soundcloudPlaylistPattern = `https?:\/\/(www\.)?soundcloud\.com\/([\w-]+)\/sets\/([\w-]+)`
|
||||||
|
|
||||||
|
// SearchService name
|
||||||
|
var soundcloudSearchServiceName = "sc"
|
||||||
|
|
||||||
// SoundCloud implements the Service interface
|
// SoundCloud implements the Service interface
|
||||||
type SoundCloud struct{}
|
type SoundCloud struct{}
|
||||||
|
|
||||||
|
@ -43,6 +46,10 @@ func (sc SoundCloud) URLRegex(url string) bool {
|
||||||
return RegexpFromURL(url, []string{soundcloudSongPattern, soundcloudPlaylistPattern}) != nil
|
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
|
// NewRequest creates the requested song/playlist and adds to the queue
|
||||||
func (sc SoundCloud) NewRequest(user *gumble.User, url string) ([]Song, error) {
|
func (sc SoundCloud) NewRequest(user *gumble.User, url string) ([]Song, error) {
|
||||||
var apiResponse *jsonq.JsonQuery
|
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
|
// 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) {
|
func (sc SoundCloud) NewSong(user *gumble.User, trackData *jsonq.JsonQuery, offset int, playlist Playlist) (Song, error) {
|
||||||
title, _ := trackData.String("title")
|
title, _ := trackData.String("title")
|
||||||
|
|
|
@ -30,6 +30,12 @@ var youtubeVideoPatterns = []string{
|
||||||
`https?:\/\/www.youtube.com\/v\/([\w-]+)(\?t=\d*m?\d*s?)?`,
|
`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
|
// YouTube implements the Service interface
|
||||||
type YouTube struct{}
|
type YouTube struct{}
|
||||||
|
|
||||||
|
@ -48,6 +54,10 @@ func (yt YouTube) URLRegex(url string) bool {
|
||||||
return RegexpFromURL(url, append(youtubeVideoPatterns, []string{youtubePlaylistPattern}...)) != nil
|
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
|
// NewRequest creates the requested song/playlist and adds to the queue
|
||||||
func (yt YouTube) NewRequest(user *gumble.User, url string) ([]Song, error) {
|
func (yt YouTube) NewRequest(user *gumble.User, url string) ([]Song, error) {
|
||||||
var songArray []Song
|
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.
|
// 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) {
|
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)
|
url := fmt.Sprintf("https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails&id=%s&key=%s", id, dj.conf.ServiceKeys.Youtube)
|
||||||
|
|
|
@ -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.
|
// 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."
|
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
|
// 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."
|
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/>
|
const HELP_HTML = `<br/>
|
||||||
<b>User Commands:</b>
|
<b>User Commands:</b>
|
||||||
<p><b>!help</b> - Displays this help.</p>
|
<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>!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>!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>
|
<p><b>!skip</b> - Casts a vote to skip the current song</p>
|
||||||
|
|
|
@ -257,6 +257,9 @@ func PerformGetRequest(url string) (*jsonq.JsonQuery, error) {
|
||||||
if response.StatusCode == 200 {
|
if response.StatusCode == 200 {
|
||||||
if body, err := ioutil.ReadAll(response.Body); err == nil {
|
if body, err := ioutil.ReadAll(response.Body); err == nil {
|
||||||
jsonString = string(body)
|
jsonString = string(body)
|
||||||
|
if jsonString[0] == '[' {
|
||||||
|
jsonString = "{\"json\":" + jsonString + "}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if response.StatusCode == 403 {
|
if response.StatusCode == 403 {
|
||||||
|
|
Reference in a new issue