Merge c837068573
into dcd2e1315f
commit
ac8852b298
|
@ -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`
|
||||
|
|
24
commands.go
24
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) {
|
||||
|
@ -246,6 +254,21 @@ func addNext(user *gumble.User, url string) error {
|
|||
}
|
||||
}
|
||||
|
||||
// searchSong performs !search functionality. Checks input searchString for service, and searches
|
||||
// for a song or video and tries to add the first result to the playlist.
|
||||
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) {
|
||||
|
@ -531,4 +554,3 @@ func listSongs(user *gumble.User, value string) {
|
|||
func version(user *gumble.User) {
|
||||
dj.SendPrivateMessage(user, DJ_VERSION)
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
@ -189,6 +193,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
|
||||
|
|
|
@ -39,6 +39,7 @@ type DjConfig struct {
|
|||
}
|
||||
Aliases struct {
|
||||
AddAlias string
|
||||
SearchAlias string
|
||||
AddNextAlias string
|
||||
SkipAlias string
|
||||
SkipPlaylistAlias string
|
||||
|
@ -67,6 +68,7 @@ type DjConfig struct {
|
|||
AdminsEnabled bool
|
||||
Admins []string
|
||||
AdminAdd bool
|
||||
AdminSearch bool
|
||||
AdminAddNext bool
|
||||
AdminAddPlaylists bool
|
||||
AdminSkip bool
|
||||
|
|
46
service.go
46
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
|
||||
|
@ -131,6 +135,48 @@ func FindServiceAndAdd(user *gumble.User, url string) error {
|
|||
}
|
||||
}
|
||||
|
||||
// FindServiceAndSearch tries to find the right service and gives the url escaped query to it.
|
||||
// The resulting string is a URL to the video/song and its supplied to the function FindServiceAndAdd
|
||||
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 searchString
|
||||
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 song/video URL
|
||||
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 {
|
||||
|
|
|
@ -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,11 @@ func (sc SoundCloud) URLRegex(url string) bool {
|
|||
return RegexpFromURL(url, []string{soundcloudSongPattern, soundcloudPlaylistPattern}) != nil
|
||||
}
|
||||
|
||||
// SearchRegex checks to see if service will accept the searchString
|
||||
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 +105,18 @@ func (sc SoundCloud) NewRequest(user *gumble.User, url string) ([]Song, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// SearchSong searches for a Song and returns the Songs URL
|
||||
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")
|
||||
|
|
|
@ -30,6 +30,12 @@ var youtubeVideoPatterns = []string{
|
|||
`https?:\/\/www.youtube.com\/v\/([\w-]+)(\?t=\d*m?\d*s?)?`,
|
||||
}
|
||||
|
||||
// SearchService name
|
||||
var youtubeSearchServiceName = "yt"
|
||||
|
||||
// SearchService video URL prefix
|
||||
var youtubeVideoURLprefix = "https://www.youtube.com/watch?v="
|
||||
|
||||
// YouTube implements the Service interface
|
||||
type YouTube struct{}
|
||||
|
||||
|
@ -48,6 +54,11 @@ func (yt YouTube) URLRegex(url string) bool {
|
|||
return RegexpFromURL(url, append(youtubeVideoPatterns, []string{youtubePlaylistPattern}...)) != nil
|
||||
}
|
||||
|
||||
// SearchRegex checks to see if service will accept the searchString
|
||||
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 +86,19 @@ func (yt YouTube) NewRequest(user *gumble.User, url string) ([]Song, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// SearchSong searches for a Song and returns the Songs URL
|
||||
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 = youtubeVideoURLprefix + 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)
|
||||
|
|
|
@ -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."
|
||||
|
||||
|
@ -135,6 +138,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>
|
||||
|
|
|
@ -261,6 +261,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 {
|
||||
|
|
Reference in New Issue