parent
9928986bfe
commit
3b0b77858b
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
mumbledj
|
||||
Goopfile.lock
|
||||
.vendor
|
||||
.project
|
4
Makefile
4
Makefile
|
@ -1,6 +1,6 @@
|
|||
all: mumbledj
|
||||
|
||||
mumbledj: main.go commands.go parseconfig.go strings.go service.go service_youtube.go songqueue.go cache.go
|
||||
mumbledj: main.go commands.go parseconfig.go strings.go service.go youtube_dl.go service_youtube.go service_soundcloud.go songqueue.go cache.go
|
||||
go get github.com/nitrous-io/goop
|
||||
rm -rf Goopfile.lock
|
||||
goop install
|
||||
|
@ -12,7 +12,7 @@ clean:
|
|||
install:
|
||||
mkdir -p ~/.mumbledj/config
|
||||
mkdir -p ~/.mumbledj/songs
|
||||
if [ -a ~/.mumbledj/config/mumbledj.gcfg ]; then mv ~/.mumbledj/config/mumbledj.gcfg ~/.mumbledj/config/mumbledj_backup.gcfg; fi;
|
||||
if [ -f ~/.mumbledj/config/mumbledj.gcfg ]; then mv ~/.mumbledj/config/mumbledj.gcfg ~/.mumbledj/config/mumbledj_backup.gcfg; fi;
|
||||
cp -u config.gcfg ~/.mumbledj/config/mumbledj.gcfg
|
||||
if [ -d ~/bin ]; then cp -f mumbledj* ~/bin/mumbledj; else sudo cp -f mumbledj* /usr/local/bin/mumbledj; fi;
|
||||
|
||||
|
|
23
README.md
23
README.md
|
@ -1,12 +1,13 @@
|
|||
MumbleDJ
|
||||
========
|
||||
**A Mumble bot that plays music fetched from YouTube videos.**
|
||||
**A Mumble bot that plays music fetched from YouTube videos and Soundcloud tracks.**
|
||||
|
||||
* [Usage](#usage)
|
||||
* [Features](#features)
|
||||
* [Commands](#commands)
|
||||
* [Installation](#installation)
|
||||
* [YouTube API Keys](#youtube-api-keys)
|
||||
* [Soundcloud API Keys](#soundcloud-api-keys)
|
||||
* [Setup Guide](#setup-guide)
|
||||
* [Update Guide](#update-guide)
|
||||
* [Troubleshooting](#troubleshooting)
|
||||
|
@ -30,7 +31,8 @@ All commandline parameters are optional. Below are descriptions of all the avail
|
|||
* `-accesstokens`: List of access tokens for the bot separated by spaces. Defaults to no access tokens.
|
||||
|
||||
## FEATURES
|
||||
* Plays audio from both YouTube videos and YouTube playlists!
|
||||
* Plays audio from YouTube and Soundcloud!
|
||||
* Supports playlists and individual videos/tracks.
|
||||
* Displays thumbnail, title, duration, submitter, and playlist title (if exists) when a new song is played.
|
||||
* Incredible customization options. Nearly everything is able to be tweaked in `~/.mumbledj/mumbledj.gcfg`.
|
||||
* A large array of [commands](#commands) that perform a wide variety of functions.
|
||||
|
@ -42,7 +44,7 @@ These are all of the chat commands currently supported by MumbleDJ. All command
|
|||
|
||||
Command | Description | Arguments | Admin | Example
|
||||
--------|-------------|-----------|-------|--------
|
||||
**add** | Adds a YouTube video's audio to the song queue. If no songs are currently in the queue, the audio will begin playing immediately. YouTube playlists may also be added using this command. Please note, however, that if a YouTube playlist contains over 25 videos only the first 25 videos will be placed in the song queue. | youtube_video_url OR youtube_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. Please note, however, that if a YouTube playlist contains over 25 videos only the first 25 videos will be placed in the song queue. | youtube_video_url OR youtube_playlist_url OR soundcloud_track_url OR soundcloud_playlist_url | No | `!add https://www.youtube.com/watch?v=5xfEr2Oxdys`
|
||||
**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`
|
||||
|
@ -84,7 +86,20 @@ Effective April 20th, 2015, all requests to YouTube's API must use v3 of their A
|
|||
|
||||
**7)** Open up `~/.bashrc` with your favorite text editor (or `~/.zshrc` if you use `zsh`). Add the following line to the bottom: `export YOUTUBE_API_KEY="<your_key_here>"`. Replace \<your_key_here\> with your API key.
|
||||
|
||||
**8)** Close your current terminal window and open another one up. You should be able to use MumbleDJ now!
|
||||
**8)** Close your current terminal window and open another one up. You should be able to use Youtube on MumbleDJ now!
|
||||
|
||||
###SOUNDCLOUD API KEYS
|
||||
A soundcloud API key is required for soundcloud integration. If no soundcloud api key is found, then the service will be disabled (youtube links will still work however).
|
||||
|
||||
**1)** Login/signup for a soundcloud account on [https://soundcloud.com](https://soundcloud.com)
|
||||
|
||||
**2)** Now to get the API key create a new app here: [http://soundcloud.com/you/apps/new](http://soundcloud.com/you/apps/new)
|
||||
|
||||
**3)** Copy the Client ID (not the Client Secret)
|
||||
|
||||
**4)** Open up `~/.bashrc` with your favorite text editor (or `~/.zshrc` if you use `zsh`). Add the following line to the bottom: `export SOUNDCLOUD_API_KEY="<your_key_here>"`. Replace \<your_key_here\> with your API key.
|
||||
|
||||
**5)** Close your current terminal window and open another one up. You should be able to use soundcloud on MumbleDJ now!
|
||||
|
||||
###SETUP GUIDE
|
||||
**1)** Install and correctly configure [`Go`](https://golang.org/) (1.4 or higher). Specifically, make sure to follow [this guide](https://golang.org/doc/code.html) and set the `GOPATH` environment variable properly.
|
||||
|
|
117
commands.go
117
commands.go
|
@ -11,7 +11,6 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -37,35 +36,35 @@ func parseCommand(user *gumble.User, username, command string) {
|
|||
// Add command
|
||||
case dj.conf.Aliases.AddAlias:
|
||||
if dj.HasPermission(username, dj.conf.Permissions.AdminAdd) {
|
||||
add(user, username, argument)
|
||||
add(user, argument)
|
||||
} else {
|
||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
||||
}
|
||||
// Skip command
|
||||
case dj.conf.Aliases.SkipAlias:
|
||||
if dj.HasPermission(username, dj.conf.Permissions.AdminSkip) {
|
||||
skip(user, username, false, false)
|
||||
skip(user, false, false)
|
||||
} else {
|
||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
||||
}
|
||||
// Skip playlist command
|
||||
case dj.conf.Aliases.SkipPlaylistAlias:
|
||||
if dj.HasPermission(username, dj.conf.Permissions.AdminAddPlaylists) {
|
||||
skip(user, username, false, true)
|
||||
skip(user, false, true)
|
||||
} else {
|
||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
||||
}
|
||||
// Forceskip command
|
||||
case dj.conf.Aliases.AdminSkipAlias:
|
||||
if dj.HasPermission(username, true) {
|
||||
skip(user, username, true, false)
|
||||
skip(user, true, false)
|
||||
} else {
|
||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
||||
}
|
||||
// Playlist forceskip command
|
||||
case dj.conf.Aliases.AdminSkipPlaylistAlias:
|
||||
if dj.HasPermission(username, true) {
|
||||
skip(user, username, true, true)
|
||||
skip(user, true, true)
|
||||
} else {
|
||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
||||
}
|
||||
|
@ -79,7 +78,7 @@ func parseCommand(user *gumble.User, username, command string) {
|
|||
// Volume command
|
||||
case dj.conf.Aliases.VolumeAlias:
|
||||
if dj.HasPermission(username, dj.conf.Permissions.AdminVolume) {
|
||||
volume(user, username, argument)
|
||||
volume(user, argument)
|
||||
} else {
|
||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
||||
}
|
||||
|
@ -158,104 +157,36 @@ func parseCommand(user *gumble.User, username, command string) {
|
|||
}
|
||||
}
|
||||
|
||||
// add performs !add functionality. Checks input URL for YouTube format, and adds
|
||||
// add performs !add functionality. Checks input URL for service, and adds
|
||||
// the URL to the queue if the format matches.
|
||||
func add(user *gumble.User, username, url string) {
|
||||
func add(user *gumble.User, url string) error {
|
||||
if url == "" {
|
||||
dj.SendPrivateMessage(user, NO_ARGUMENT_MSG)
|
||||
return errors.New("NO_ARGUMENT")
|
||||
} else {
|
||||
youtubePatterns := []string{
|
||||
`https?:\/\/www\.youtube\.com\/watch\?v=([\w-]+)(\&t=\d*m?\d*s?)?`,
|
||||
`https?:\/\/youtube\.com\/watch\?v=([\w-]+)(\&t=\d*m?\d*s?)?`,
|
||||
`https?:\/\/youtu.be\/([\w-]+)(\?t=\d*m?\d*s?)?`,
|
||||
`https?:\/\/youtube.com\/v\/([\w-]+)(\?t=\d*m?\d*s?)?`,
|
||||
`https?:\/\/www.youtube.com\/v\/([\w-]+)(\?t=\d*m?\d*s?)?`,
|
||||
}
|
||||
matchFound := false
|
||||
shortURL := ""
|
||||
startOffset := ""
|
||||
|
||||
for _, pattern := range youtubePatterns {
|
||||
if re, err := regexp.Compile(pattern); err == nil {
|
||||
if re.MatchString(url) {
|
||||
matchFound = true
|
||||
matches := re.FindAllStringSubmatch(url, -1)
|
||||
shortURL = matches[0][1]
|
||||
if len(matches[0]) == 3 {
|
||||
startOffset = matches[0][2]
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matchFound {
|
||||
if newSong, err := NewYouTubeSong(username, shortURL, startOffset, nil); err == nil {
|
||||
dj.client.Self.Channel.Send(fmt.Sprintf(SONG_ADDED_HTML, username, newSong.title), false)
|
||||
if dj.queue.Len() == 1 && !dj.audioStream.IsPlaying() {
|
||||
if err := dj.queue.CurrentSong().Download(); err == nil {
|
||||
dj.queue.CurrentSong().Play()
|
||||
} else {
|
||||
dj.SendPrivateMessage(user, AUDIO_FAIL_MSG)
|
||||
dj.queue.CurrentSong().Delete()
|
||||
dj.queue.OnSongFinished()
|
||||
}
|
||||
}
|
||||
} else if fmt.Sprint(err) == "Song exceeds the maximum allowed duration." {
|
||||
dj.SendPrivateMessage(user, VIDEO_TOO_LONG_MSG)
|
||||
} else if fmt.Sprint(err) == "Invalid API key supplied." {
|
||||
dj.SendPrivateMessage(user, INVALID_API_KEY)
|
||||
} else {
|
||||
dj.SendPrivateMessage(user, INVALID_YOUTUBE_ID_MSG)
|
||||
}
|
||||
} else {
|
||||
// Check to see if we have a playlist URL instead.
|
||||
youtubePlaylistPattern := `https?:\/\/www\.youtube\.com\/playlist\?list=([\w-]+)`
|
||||
if re, err := regexp.Compile(youtubePlaylistPattern); err == nil {
|
||||
if re.MatchString(url) {
|
||||
if dj.HasPermission(username, dj.conf.Permissions.AdminAddPlaylists) {
|
||||
shortURL = re.FindStringSubmatch(url)[1]
|
||||
oldLength := dj.queue.Len()
|
||||
if newPlaylist, err := NewYouTubePlaylist(username, shortURL); err == nil {
|
||||
dj.client.Self.Channel.Send(fmt.Sprintf(PLAYLIST_ADDED_HTML, username, newPlaylist.title), false)
|
||||
if oldLength == 0 && dj.queue.Len() != 0 && !dj.audioStream.IsPlaying() {
|
||||
if err := dj.queue.CurrentSong().Download(); err == nil {
|
||||
dj.queue.CurrentSong().Play()
|
||||
} else {
|
||||
dj.SendPrivateMessage(user, AUDIO_FAIL_MSG)
|
||||
dj.queue.CurrentSong().Delete()
|
||||
dj.queue.OnSongFinished()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dj.SendPrivateMessage(user, INVALID_YOUTUBE_ID_MSG)
|
||||
}
|
||||
} else {
|
||||
dj.SendPrivateMessage(user, NO_PLAYLIST_PERMISSION_MSG)
|
||||
}
|
||||
} else {
|
||||
dj.SendPrivateMessage(user, INVALID_URL_MSG)
|
||||
}
|
||||
}
|
||||
err := FindServiceAndAdd(user, url)
|
||||
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, username string, admin, playlistSkip bool) {
|
||||
func skip(user *gumble.User, admin, playlistSkip bool) {
|
||||
if dj.audioStream.IsPlaying() {
|
||||
if playlistSkip {
|
||||
if dj.queue.CurrentSong().Playlist() != nil {
|
||||
if err := dj.queue.CurrentSong().Playlist().AddSkip(username); err == nil {
|
||||
if err := dj.queue.CurrentSong().Playlist().AddSkip(user.Name); err == nil {
|
||||
submitterSkipped := false
|
||||
if admin {
|
||||
dj.client.Self.Channel.Send(ADMIN_PLAYLIST_SKIP_MSG, false)
|
||||
} else if dj.queue.CurrentSong().Submitter() == username {
|
||||
dj.client.Self.Channel.Send(fmt.Sprintf(PLAYLIST_SUBMITTER_SKIP_HTML, username), false)
|
||||
} else if dj.queue.CurrentSong().Submitter() == user.Name {
|
||||
dj.client.Self.Channel.Send(fmt.Sprintf(PLAYLIST_SUBMITTER_SKIP_HTML, user.Name), false)
|
||||
submitterSkipped = true
|
||||
} else {
|
||||
dj.client.Self.Channel.Send(fmt.Sprintf(PLAYLIST_SKIP_ADDED_HTML, username), false)
|
||||
dj.client.Self.Channel.Send(fmt.Sprintf(PLAYLIST_SKIP_ADDED_HTML, user.Name), false)
|
||||
}
|
||||
if submitterSkipped || dj.queue.CurrentSong().Playlist().SkipReached(len(dj.client.Self.Channel.Users)) || admin {
|
||||
id := dj.queue.CurrentSong().Playlist().ID()
|
||||
|
@ -284,15 +215,15 @@ func skip(user *gumble.User, username string, admin, playlistSkip bool) {
|
|||
dj.SendPrivateMessage(user, NO_PLAYLIST_PLAYING_MSG)
|
||||
}
|
||||
} else {
|
||||
if err := dj.queue.CurrentSong().AddSkip(username); err == nil {
|
||||
if err := dj.queue.CurrentSong().AddSkip(user.Name); err == nil {
|
||||
submitterSkipped := false
|
||||
if admin {
|
||||
dj.client.Self.Channel.Send(ADMIN_SONG_SKIP_MSG, false)
|
||||
} else if dj.queue.CurrentSong().Submitter() == username {
|
||||
dj.client.Self.Channel.Send(fmt.Sprintf(SUBMITTER_SKIP_HTML, username), false)
|
||||
} else if dj.queue.CurrentSong().Submitter() == user.Name {
|
||||
dj.client.Self.Channel.Send(fmt.Sprintf(SUBMITTER_SKIP_HTML, user.Name), false)
|
||||
submitterSkipped = true
|
||||
} else {
|
||||
dj.client.Self.Channel.Send(fmt.Sprintf(SKIP_ADDED_HTML, username), false)
|
||||
dj.client.Self.Channel.Send(fmt.Sprintf(SKIP_ADDED_HTML, user.Name), false)
|
||||
}
|
||||
if submitterSkipped || dj.queue.CurrentSong().SkipReached(len(dj.client.Self.Channel.Users)) || admin {
|
||||
if !(submitterSkipped || admin) {
|
||||
|
@ -317,7 +248,7 @@ func help(user *gumble.User) {
|
|||
// volume performs !volume functionality. Checks input value against LowestVolume and HighestVolume from
|
||||
// config to determine if the volume should be applied. If in the correct range, the new volume
|
||||
// is applied and is immediately in effect.
|
||||
func volume(user *gumble.User, username, value string) {
|
||||
func volume(user *gumble.User, value string) {
|
||||
if value == "" {
|
||||
dj.client.Self.Channel.Send(fmt.Sprintf(CUR_VOLUME_HTML, dj.audioStream.Volume), false)
|
||||
} else {
|
||||
|
@ -325,7 +256,7 @@ func volume(user *gumble.User, username, value string) {
|
|||
newVolume := float32(parsedVolume)
|
||||
if newVolume >= dj.conf.Volume.LowestVolume && newVolume <= dj.conf.Volume.HighestVolume {
|
||||
dj.audioStream.Volume = newVolume
|
||||
dj.client.Self.Channel.Send(fmt.Sprintf(VOLUME_SUCCESS_HTML, username, dj.audioStream.Volume), false)
|
||||
dj.client.Self.Channel.Send(fmt.Sprintf(VOLUME_SUCCESS_HTML, user.Name, dj.audioStream.Volume), false)
|
||||
} else {
|
||||
dj.SendPrivateMessage(user, fmt.Sprintf(NOT_IN_VOLUME_RANGE_MSG, dj.conf.Volume.LowestVolume, dj.conf.Volume.HighestVolume))
|
||||
}
|
||||
|
|
42
main.go
42
main.go
|
@ -13,6 +13,7 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -100,7 +101,7 @@ func (dj *mumbledj) OnTextMessage(e *gumble.TextMessageEvent) {
|
|||
func (dj *mumbledj) OnUserChange(e *gumble.UserChangeEvent) {
|
||||
if e.Type.Has(gumble.UserChangeDisconnected) {
|
||||
if dj.audioStream.IsPlaying() {
|
||||
if dj.queue.CurrentSong().Playlist() != nil {
|
||||
if !isNil(dj.queue.CurrentSong().Playlist()) {
|
||||
dj.queue.CurrentSong().Playlist().RemoveSkip(e.User.Name)
|
||||
}
|
||||
dj.queue.CurrentSong().RemoveSkip(e.User.Name)
|
||||
|
@ -130,15 +131,44 @@ func (dj *mumbledj) SendPrivateMessage(user *gumble.User, message string) {
|
|||
}
|
||||
}
|
||||
|
||||
// PerformStartupChecks checks the MumbleDJ installation to ensure proper usage.
|
||||
func PerformStartupChecks() {
|
||||
// CheckAPIKeys enables the services with API keys in the environment varaibles
|
||||
func CheckAPIKeys() {
|
||||
anyDisabled := false
|
||||
|
||||
// Checks YouTube API key
|
||||
if os.Getenv("YOUTUBE_API_KEY") == "" {
|
||||
fmt.Printf("You do not have a YouTube API key defined in your environment variables.\n" +
|
||||
"Please see the following link for info on how to fix this: https://github.com/matthieugrieger/mumbledj#youtube-api-keys\n")
|
||||
anyDisabled = true
|
||||
fmt.Printf("The youtube service has been disabled as you do not have a YouTube API key defined in your environment variables.\n")
|
||||
} else {
|
||||
services = append(services, YouTube{})
|
||||
}
|
||||
|
||||
// Checks Soundcloud API key
|
||||
if os.Getenv("SOUNDCLOUD_API_KEY") == "" {
|
||||
anyDisabled = true
|
||||
fmt.Printf("The soundcloud service has been disabled as you do not have a Soundcloud API key defined in your environment variables.\n")
|
||||
} else {
|
||||
services = append(services, SoundCloud{})
|
||||
}
|
||||
|
||||
// Checks to see if any service was disabled
|
||||
if anyDisabled {
|
||||
fmt.Printf("Please see the following link for info on how to enable missing services: https://github.com/matthieugrieger/mumbledj\n")
|
||||
}
|
||||
|
||||
// Exits application if no services are enabled
|
||||
if services == nil {
|
||||
fmt.Printf("No services are enabled, and thus closing\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// isNil checks to see if an object is nil
|
||||
func isNil(a interface{}) bool {
|
||||
defer func() { recover() }()
|
||||
return a == nil || reflect.ValueOf(a).IsNil()
|
||||
}
|
||||
|
||||
// dj variable declaration. This is done outside of main() to allow global use.
|
||||
var dj = mumbledj{
|
||||
keepAlive: make(chan bool),
|
||||
|
@ -151,7 +181,7 @@ var dj = mumbledj{
|
|||
// args, sets up the gumble client and its listeners, and then connects to the server.
|
||||
func main() {
|
||||
|
||||
PerformStartupChecks()
|
||||
CheckAPIKeys()
|
||||
|
||||
if currentUser, err := user.Current(); err == nil {
|
||||
dj.homeDir = currentUser.HomeDir
|
||||
|
|
103
service.go
103
service.go
|
@ -7,6 +7,23 @@
|
|||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/layeh/gumble/gumble"
|
||||
)
|
||||
|
||||
// Service interface. Each service will implement these functions
|
||||
type Service interface {
|
||||
ServiceName() string
|
||||
TrackName() string
|
||||
URLRegex(string) bool
|
||||
NewRequest(*gumble.User, string) ([]Song, error)
|
||||
}
|
||||
|
||||
// Song interface. Each service will implement these
|
||||
// functions in their Song types.
|
||||
type Song interface {
|
||||
|
@ -20,7 +37,7 @@ type Song interface {
|
|||
Title() string
|
||||
ID() string
|
||||
Filename() string
|
||||
Duration() string
|
||||
Duration() time.Duration
|
||||
Thumbnail() string
|
||||
Playlist() Playlist
|
||||
DontSkip() bool
|
||||
|
@ -37,3 +54,87 @@ type Playlist interface {
|
|||
ID() string
|
||||
Title() string
|
||||
}
|
||||
|
||||
var services []Service
|
||||
|
||||
// FindServiceAndAdd tries the given url with each service
|
||||
// and adds the song/playlist with the correct service
|
||||
func FindServiceAndAdd(user *gumble.User, url string) error {
|
||||
var urlService Service
|
||||
|
||||
// Checks all services to see if any can take the URL
|
||||
for _, service := range services {
|
||||
if service.URLRegex(url) {
|
||||
urlService = service
|
||||
}
|
||||
}
|
||||
|
||||
if urlService == nil {
|
||||
return errors.New(INVALID_URL_MSG)
|
||||
} else {
|
||||
var title string
|
||||
var songsAdded = 0
|
||||
var songArray []Song
|
||||
var err error
|
||||
|
||||
// Get service to create songs
|
||||
if songArray, err = urlService.NewRequest(user, url); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check Playlist Permission
|
||||
if len(songArray) > 1 && !dj.HasPermission(user.Name, dj.conf.Permissions.AdminAddPlaylists) {
|
||||
return errors.New(NO_PLAYLIST_PERMISSION_MSG)
|
||||
}
|
||||
|
||||
// Loop through all songs and add to the queue
|
||||
oldLength := dj.queue.Len()
|
||||
for _, song := range songArray {
|
||||
// Check song is not too long
|
||||
if dj.conf.General.MaxSongDuration == 0 || int(song.Duration().Seconds()) <= dj.conf.General.MaxSongDuration {
|
||||
if !isNil(song.Playlist()) {
|
||||
title = song.Playlist().Title()
|
||||
} else {
|
||||
title = song.Title()
|
||||
}
|
||||
|
||||
// Add song to queue
|
||||
dj.queue.AddSong(song)
|
||||
songsAdded++
|
||||
}
|
||||
}
|
||||
|
||||
// Alert channel of added song/playlist
|
||||
if songsAdded == 0 {
|
||||
return errors.New(fmt.Sprintf(TRACK_TOO_LONG_MSG, urlService.ServiceName()))
|
||||
} else if songsAdded == 1 {
|
||||
dj.client.Self.Channel.Send(fmt.Sprintf(SONG_ADDED_HTML, user.Name, title), false)
|
||||
} else {
|
||||
dj.client.Self.Channel.Send(fmt.Sprintf(PLAYLIST_ADDED_HTML, user.Name, title), false)
|
||||
}
|
||||
|
||||
// Starts playing the new song if nothing else is playing
|
||||
if oldLength == 0 && dj.queue.Len() != 0 && !dj.audioStream.IsPlaying() {
|
||||
if err := dj.queue.CurrentSong().Download(); err == nil {
|
||||
dj.queue.CurrentSong().Play()
|
||||
} else {
|
||||
dj.queue.CurrentSong().Delete()
|
||||
dj.queue.OnSongFinished()
|
||||
return errors.New(AUDIO_FAIL_MSG)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// RegexpFromURL loops through an array of patterns to see if it matches the url
|
||||
func RegexpFromURL(url string, patterns []string) *regexp.Regexp {
|
||||
for _, pattern := range patterns {
|
||||
if re, err := regexp.Compile(pattern); err == nil {
|
||||
if re.MatchString(url) {
|
||||
return re
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
126
service_soundcloud.go
Normal file
126
service_soundcloud.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* MumbleDJ
|
||||
* By Matthieu Grieger
|
||||
* service_soundcloud.go
|
||||
* Copyright (c) 2014, 2015 Matthieu Grieger (MIT License)
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jmoiron/jsonq"
|
||||
"github.com/layeh/gumble/gumble"
|
||||
)
|
||||
|
||||
// Regular expressions for soundcloud urls
|
||||
var soundcloudSongPattern = `https?:\/\/(www\.)?soundcloud\.com\/([\w-]+)\/([\w-]+)(#t=\n\n?(:\n\n)*)?`
|
||||
var soundcloudPlaylistPattern = `https?:\/\/(www\.)?soundcloud\.com\/([\w-]+)\/sets\/([\w-]+)`
|
||||
|
||||
// SoundCloud implements the Service interface
|
||||
type SoundCloud struct{}
|
||||
|
||||
// ------------------
|
||||
// SOUNDCLOUD SERVICE
|
||||
// ------------------
|
||||
|
||||
// ServiceName is the human readable version of the service name
|
||||
func (sc SoundCloud) ServiceName() string {
|
||||
return "Soundcloud"
|
||||
}
|
||||
|
||||
// TrackName is the human readable version of the service name
|
||||
func (sc SoundCloud) TrackName() string {
|
||||
return "Song"
|
||||
}
|
||||
|
||||
// URLRegex checks to see if service will accept URL
|
||||
func (sc SoundCloud) URLRegex(url string) bool {
|
||||
return RegexpFromURL(url, []string{soundcloudSongPattern, soundcloudPlaylistPattern}) != nil
|
||||
}
|
||||
|
||||
// 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
|
||||
var songArray []Song
|
||||
var err error
|
||||
timesplit := strings.Split(url, "#t=")
|
||||
url = fmt.Sprintf("http://api.soundcloud.com/resolve?url=%s&client_id=%s", timesplit[0], os.Getenv("SOUNDCLOUD_API_KEY"))
|
||||
if apiResponse, err = PerformGetRequest(url); err != nil {
|
||||
return nil, errors.New(fmt.Sprintf(INVALID_API_KEY, sc.ServiceName()))
|
||||
}
|
||||
|
||||
tracks, err := apiResponse.ArrayOfObjects("tracks")
|
||||
if err == nil {
|
||||
// PLAYLIST
|
||||
// Create playlist
|
||||
title, _ := apiResponse.String("title")
|
||||
permalink, _ := apiResponse.String("permalink_url")
|
||||
playlist := &AudioPlaylist{
|
||||
id: permalink,
|
||||
title: title,
|
||||
}
|
||||
|
||||
// Add all tracks
|
||||
for _, t := range tracks {
|
||||
if song, err := sc.NewSong(user, jsonq.NewQuery(t), 0, playlist); err == nil {
|
||||
songArray = append(songArray, song)
|
||||
}
|
||||
}
|
||||
return songArray, nil
|
||||
} else {
|
||||
// SONG
|
||||
// Calculate offset
|
||||
offset := 0
|
||||
if len(timesplit) == 2 {
|
||||
timesplit = strings.Split(timesplit[1], ":")
|
||||
multiplier := 1
|
||||
for i := len(timesplit) - 1; i >= 0; i-- {
|
||||
time, _ := strconv.Atoi(timesplit[i])
|
||||
offset += time * multiplier
|
||||
multiplier *= 60
|
||||
}
|
||||
}
|
||||
|
||||
// Add the track
|
||||
if song, err := sc.NewSong(user, apiResponse, offset, nil); err == nil {
|
||||
return append(songArray, song), err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
id, _ := trackData.Int("id")
|
||||
durationMS, _ := trackData.Int("duration")
|
||||
url, _ := trackData.String("permalink_url")
|
||||
thumbnail, err := trackData.String("artwork_url")
|
||||
if err != nil {
|
||||
// Song has no artwork, using profile avatar instead
|
||||
userObj, _ := trackData.Object("user")
|
||||
thumbnail, _ = jsonq.NewQuery(userObj).String("avatar_url")
|
||||
}
|
||||
|
||||
song := &AudioTrack{
|
||||
id: strconv.Itoa(id),
|
||||
title: title,
|
||||
url: url,
|
||||
thumbnail: thumbnail,
|
||||
submitter: user,
|
||||
duration: durationMS / 1000,
|
||||
offset: offset,
|
||||
format: "mp3",
|
||||
playlist: playlist,
|
||||
skippers: make([]string, 0),
|
||||
dontSkip: false,
|
||||
service: sc,
|
||||
}
|
||||
return song, nil
|
||||
}
|
|
@ -8,355 +8,107 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/jsonq"
|
||||
"github.com/layeh/gumble/gumble_ffmpeg"
|
||||
"github.com/layeh/gumble/gumble"
|
||||
)
|
||||
|
||||
// ------------
|
||||
// YOUTUBE SONG
|
||||
// ------------
|
||||
|
||||
// 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
|
||||
// Regular expressions for youtube urls
|
||||
var youtubePlaylistPattern = `https?:\/\/www\.youtube\.com\/playlist\?list=([\w-]+)`
|
||||
var youtubeVideoPatterns = []string{
|
||||
`https?:\/\/www\.youtube\.com\/watch\?v=([\w-]+)(\&t=\d*m?\d*s?)?`,
|
||||
`https?:\/\/youtube\.com\/watch\?v=([\w-]+)(\&t=\d*m?\d*s?)?`,
|
||||
`https?:\/\/youtu.be\/([\w-]+)(\?t=\d*m?\d*s?)?`,
|
||||
`https?:\/\/youtube.com\/v\/([\w-]+)(\?t=\d*m?\d*s?)?`,
|
||||
`https?:\/\/www.youtube.com\/v\/([\w-]+)(\?t=\d*m?\d*s?)?`,
|
||||
}
|
||||
|
||||
// NewYouTubeSong gathers the metadata for a song extracted from a YouTube video, and returns
|
||||
// the song.
|
||||
func NewYouTubeSong(user, id, offset string, playlist *YouTubePlaylist) (*YouTubeSong, 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",
|
||||
id, os.Getenv("YOUTUBE_API_KEY"))
|
||||
if apiResponse, err = PerformGetRequest(url); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// YouTube implements the Service interface
|
||||
type YouTube struct{}
|
||||
|
||||
var offsetDays, offsetHours, offsetMinutes, offsetSeconds int64
|
||||
if offset != "" {
|
||||
offsetExp := regexp.MustCompile(`t\=(?P<days>\d+d)?(?P<hours>\d+h)?(?P<minutes>\d+m)?(?P<seconds>\d+s)?`)
|
||||
offsetMatch := offsetExp.FindStringSubmatch(offset)
|
||||
offsetResult := make(map[string]string)
|
||||
for i, name := range offsetExp.SubexpNames() {
|
||||
if i < len(offsetMatch) {
|
||||
offsetResult[name] = offsetMatch[i]
|
||||
// ServiceName is the human readable version of the service name
|
||||
func (yt YouTube) ServiceName() string {
|
||||
return "YouTube"
|
||||
}
|
||||
|
||||
// TrackName is the human readable version of the service name
|
||||
func (yt YouTube) TrackName() string {
|
||||
return "Video"
|
||||
}
|
||||
|
||||
// URLRegex checks to see if service will accept URL
|
||||
func (yt YouTube) URLRegex(url string) bool {
|
||||
return RegexpFromURL(url, append(youtubeVideoPatterns, []string{youtubePlaylistPattern}...)) != nil
|
||||
}
|
||||
|
||||
// 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
|
||||
var shortURL, startOffset = "", ""
|
||||
if re, err := regexp.Compile(youtubePlaylistPattern); err == nil {
|
||||
if re.MatchString(url) {
|
||||
shortURL = re.FindStringSubmatch(url)[1]
|
||||
return yt.NewPlaylist(user, shortURL)
|
||||
} else {
|
||||
re = RegexpFromURL(url, youtubeVideoPatterns)
|
||||
matches := re.FindAllStringSubmatch(url, -1)
|
||||
shortURL = matches[0][1]
|
||||
if len(matches[0]) == 3 {
|
||||
startOffset = matches[0][2]
|
||||
}
|
||||
song, err := yt.NewSong(user, shortURL, startOffset, nil)
|
||||
if !isNil(song) {
|
||||
return append(songArray, song), nil
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if offsetResult["days"] != "" {
|
||||
offsetDays, _ = strconv.ParseInt(strings.TrimSuffix(offsetResult["days"], "d"), 10, 32)
|
||||
}
|
||||
if offsetResult["hours"] != "" {
|
||||
offsetHours, _ = strconv.ParseInt(strings.TrimSuffix(offsetResult["hours"], "h"), 10, 32)
|
||||
}
|
||||
if offsetResult["minutes"] != "" {
|
||||
offsetMinutes, _ = strconv.ParseInt(strings.TrimSuffix(offsetResult["minutes"], "m"), 10, 32)
|
||||
}
|
||||
if offsetResult["seconds"] != "" {
|
||||
offsetSeconds, _ = strconv.ParseInt(strings.TrimSuffix(offsetResult["seconds"], "s"), 10, 32)
|
||||
}
|
||||
}
|
||||
|
||||
title, _ := apiResponse.String("items", "0", "snippet", "title")
|
||||
thumbnail, _ := apiResponse.String("items", "0", "snippet", "thumbnails", "high", "url")
|
||||
duration, _ := apiResponse.String("items", "0", "contentDetails", "duration")
|
||||
|
||||
var days, hours, minutes, seconds int64
|
||||
timestampExp := regexp.MustCompile(`P(?P<days>\d+D)?T(?P<hours>\d+H)?(?P<minutes>\d+M)?(?P<seconds>\d+S)?`)
|
||||
timestampMatch := timestampExp.FindStringSubmatch(duration)
|
||||
timestampResult := make(map[string]string)
|
||||
for i, name := range timestampExp.SubexpNames() {
|
||||
if i < len(timestampMatch) {
|
||||
timestampResult[name] = timestampMatch[i]
|
||||
}
|
||||
}
|
||||
|
||||
if timestampResult["days"] != "" {
|
||||
days, _ = strconv.ParseInt(strings.TrimSuffix(timestampResult["days"], "D"), 10, 32)
|
||||
}
|
||||
if timestampResult["hours"] != "" {
|
||||
hours, _ = strconv.ParseInt(strings.TrimSuffix(timestampResult["hours"], "H"), 10, 32)
|
||||
}
|
||||
if timestampResult["minutes"] != "" {
|
||||
minutes, _ = strconv.ParseInt(strings.TrimSuffix(timestampResult["minutes"], "M"), 10, 32)
|
||||
}
|
||||
if timestampResult["seconds"] != "" {
|
||||
seconds, _ = strconv.ParseInt(strings.TrimSuffix(timestampResult["seconds"], "S"), 10, 32)
|
||||
}
|
||||
|
||||
totalSeconds := int((days * 86400) + (hours * 3600) + (minutes * 60) + seconds)
|
||||
var durationString string
|
||||
if hours != 0 {
|
||||
if days != 0 {
|
||||
durationString = fmt.Sprintf("%d:%02d:%02d:%02d", days, hours, minutes, seconds)
|
||||
} else {
|
||||
durationString = fmt.Sprintf("%d:%02d:%02d", hours, minutes, seconds)
|
||||
}
|
||||
} else {
|
||||
durationString = fmt.Sprintf("%d:%02d", minutes, seconds)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if dj.conf.General.MaxSongDuration == 0 || totalSeconds <= dj.conf.General.MaxSongDuration {
|
||||
song := &YouTubeSong{
|
||||
// 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, os.Getenv("YOUTUBE_API_KEY"))
|
||||
if apiResponse, err := PerformGetRequest(url); err == nil {
|
||||
title, _ := apiResponse.String("items", "0", "snippet", "title")
|
||||
thumbnail, _ := apiResponse.String("items", "0", "snippet", "thumbnails", "high", "url")
|
||||
duration, _ := apiResponse.String("items", "0", "contentDetails", "duration")
|
||||
|
||||
song := &AudioTrack{
|
||||
submitter: user,
|
||||
title: title,
|
||||
id: id,
|
||||
offset: int((offsetDays * 86400) + (offsetHours * 3600) + (offsetMinutes * 60) + offsetSeconds),
|
||||
filename: id + ".m4a",
|
||||
duration: durationString,
|
||||
url: "https://youtu.be/" + id,
|
||||
offset: int(yt.parseTime(offset, `\?T\=(?P<days>\d+D)?(?P<hours>\d+H)?(?P<minutes>\d+M)?(?P<seconds>\d+S)?`).Seconds()),
|
||||
duration: int(yt.parseTime(duration, `P(?P<days>\d+D)?T(?P<hours>\d+H)?(?P<minutes>\d+M)?(?P<seconds>\d+S)?`).Seconds()),
|
||||
thumbnail: thumbnail,
|
||||
format: "m4a",
|
||||
skippers: make([]string, 0),
|
||||
playlist: nil,
|
||||
playlist: playlist,
|
||||
dontSkip: false,
|
||||
service: yt,
|
||||
}
|
||||
dj.queue.AddSong(song)
|
||||
|
||||
return song, nil
|
||||
}
|
||||
return nil, errors.New("Song exceeds the maximum allowed duration.")
|
||||
return nil, errors.New(fmt.Sprintf(INVALID_API_KEY, yt.ServiceName()))
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if _, err := os.Stat(fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, s.Filename())); os.IsNotExist(err) {
|
||||
cmd := exec.Command("youtube-dl", "--no-mtime", "--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()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
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 s.Playlist() == nil {
|
||||
message := `
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><img src="%s" width=150 /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><b><a href="http://youtu.be/%s">%s</a> (%s)</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">Added by %s</td>
|
||||
</tr>
|
||||
</table>
|
||||
`
|
||||
dj.client.Self.Channel.Send(fmt.Sprintf(message, s.Thumbnail(), s.ID(), s.Title(),
|
||||
s.Duration(), s.Submitter()), false)
|
||||
} else {
|
||||
message := `
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><img src="%s" width=150 /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><b><a href="http://youtu.be/%s">%s</a> (%s)</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">Added by %s</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">From playlist "%s"</td>
|
||||
</tr>
|
||||
</table>
|
||||
`
|
||||
dj.client.Self.Channel.Send(fmt.Sprintf(message, s.Thumbnail(), s.ID(),
|
||||
s.Title(), s.Duration(), s.Submitter(), s.Playlist().Title()), false)
|
||||
}
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
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
|
||||
// ----------------
|
||||
|
||||
// YouTubePlaylist holds the metadata for a YouTube playlist.
|
||||
type YouTubePlaylist struct {
|
||||
id string
|
||||
title string
|
||||
}
|
||||
|
||||
// NewYouTubePlaylist gathers the metadata for a YouTube playlist and returns it.
|
||||
func NewYouTubePlaylist(user, id string) (*YouTubePlaylist, error) {
|
||||
var apiResponse *jsonq.JsonQuery
|
||||
var err error
|
||||
// Retrieve title of playlist
|
||||
url := fmt.Sprintf("https://www.googleapis.com/youtube/v3/playlists?part=snippet&id=%s&key=%s",
|
||||
id, os.Getenv("YOUTUBE_API_KEY"))
|
||||
if apiResponse, err = PerformGetRequest(url); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
title, _ := apiResponse.String("items", "0", "snippet", "title")
|
||||
|
||||
playlist := &YouTubePlaylist{
|
||||
id: id,
|
||||
title: title,
|
||||
}
|
||||
|
||||
// Retrieve items in playlist
|
||||
url = fmt.Sprintf("https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=25&playlistId=%s&key=%s",
|
||||
id, os.Getenv("YOUTUBE_API_KEY"))
|
||||
if apiResponse, err = PerformGetRequest(url); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
numVideos, _ := apiResponse.Int("pageInfo", "totalResults")
|
||||
if numVideos > 25 {
|
||||
numVideos = 25
|
||||
}
|
||||
|
||||
for i := 0; i < numVideos; i++ {
|
||||
index := strconv.Itoa(i)
|
||||
videoTitle, err := apiResponse.String("items", index, "snippet", "title")
|
||||
videoID, _ := apiResponse.String("items", index, "snippet", "resourceId", "videoId")
|
||||
videoThumbnail, _ := apiResponse.String("items", index, "snippet", "thumbnails", "high", "url")
|
||||
|
||||
// A completely separate API call just to get the duration of a video in a
|
||||
// playlist? WHY GOOGLE, WHY?!
|
||||
var durationResponse *jsonq.JsonQuery
|
||||
url = fmt.Sprintf("https://www.googleapis.com/youtube/v3/videos?part=contentDetails&id=%s&key=%s",
|
||||
videoID, os.Getenv("YOUTUBE_API_KEY"))
|
||||
if durationResponse, err = PerformGetRequest(url); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
videoDuration, _ := durationResponse.String("items", "0", "contentDetails", "duration")
|
||||
|
||||
var days, hours, minutes, seconds int64
|
||||
timestampExp := regexp.MustCompile(`P(?P<days>\d+D)?T(?P<hours>\d+H)?(?P<minutes>\d+M)?(?P<seconds>\d+S)?`)
|
||||
timestampMatch := timestampExp.FindStringSubmatch(videoDuration)
|
||||
// parseTime converts from the string youtube returns to a time.Duration
|
||||
func (yt YouTube) parseTime(duration, regex string) time.Duration {
|
||||
var days, hours, minutes, seconds, totalSeconds int64
|
||||
if duration != "" {
|
||||
timestampExp := regexp.MustCompile(regex)
|
||||
timestampMatch := timestampExp.FindStringSubmatch(strings.ToUpper(duration))
|
||||
timestampResult := make(map[string]string)
|
||||
for i, name := range timestampExp.SubexpNames() {
|
||||
if i < len(timestampMatch) {
|
||||
|
@ -377,112 +129,48 @@ func NewYouTubePlaylist(user, id string) (*YouTubePlaylist, error) {
|
|||
seconds, _ = strconv.ParseInt(strings.TrimSuffix(timestampResult["seconds"], "S"), 10, 32)
|
||||
}
|
||||
|
||||
totalSeconds := int((days * 86400) + (hours * 3600) + (minutes * 60) + seconds)
|
||||
var durationString string
|
||||
if hours != 0 {
|
||||
if days != 0 {
|
||||
durationString = fmt.Sprintf("%d:%02d:%02d:%02d", days, hours, minutes, seconds)
|
||||
} else {
|
||||
durationString = fmt.Sprintf("%d:%02d:%02d", hours, minutes, seconds)
|
||||
}
|
||||
} else {
|
||||
durationString = fmt.Sprintf("%d:%02d", minutes, seconds)
|
||||
}
|
||||
|
||||
if dj.conf.General.MaxSongDuration == 0 || totalSeconds <= dj.conf.General.MaxSongDuration {
|
||||
playlistSong := &YouTubeSong{
|
||||
submitter: user,
|
||||
title: videoTitle,
|
||||
id: videoID,
|
||||
filename: videoID + ".m4a",
|
||||
duration: durationString,
|
||||
thumbnail: videoThumbnail,
|
||||
skippers: make([]string, 0),
|
||||
playlist: playlist,
|
||||
dontSkip: false,
|
||||
}
|
||||
dj.queue.AddSong(playlistSong)
|
||||
}
|
||||
}
|
||||
return playlist, nil
|
||||
}
|
||||
|
||||
// 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 := ""
|
||||
|
||||
if response, err := http.Get(url); err == nil {
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode == 200 {
|
||||
if body, err := ioutil.ReadAll(response.Body); err == nil {
|
||||
jsonString = string(body)
|
||||
}
|
||||
} else {
|
||||
if response.StatusCode == 403 {
|
||||
return nil, errors.New("Invalid API key supplied.")
|
||||
}
|
||||
return nil, errors.New("Invalid YouTube ID supplied.")
|
||||
}
|
||||
totalSeconds = int64((days * 86400) + (hours * 3600) + (minutes * 60) + seconds)
|
||||
} else {
|
||||
return nil, errors.New("An error occurred while receiving HTTP GET response.")
|
||||
totalSeconds = 0
|
||||
}
|
||||
output, _ := time.ParseDuration(strconv.Itoa(int(totalSeconds)) + "s")
|
||||
return output
|
||||
}
|
||||
|
||||
// NewPlaylist gathers the metadata for a YouTube playlist and returns it.
|
||||
func (yt YouTube) NewPlaylist(user *gumble.User, id string) ([]Song, error) {
|
||||
var apiResponse *jsonq.JsonQuery
|
||||
var songArray []Song
|
||||
var err error
|
||||
// Retrieve title of playlist
|
||||
url := fmt.Sprintf("https://www.googleapis.com/youtube/v3/playlists?part=snippet&id=%s&key=%s", id, os.Getenv("YOUTUBE_API_KEY"))
|
||||
if apiResponse, err = PerformGetRequest(url); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
title, _ := apiResponse.String("items", "0", "snippet", "title")
|
||||
|
||||
playlist := &AudioPlaylist{
|
||||
id: id,
|
||||
title: title,
|
||||
}
|
||||
|
||||
jsonData := map[string]interface{}{}
|
||||
decoder := json.NewDecoder(strings.NewReader(jsonString))
|
||||
decoder.Decode(&jsonData)
|
||||
jq := jsonq.NewQuery(jsonData)
|
||||
// Retrieve items in playlist
|
||||
url = fmt.Sprintf("https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=50&playlistId=%s&key=%s",
|
||||
id, os.Getenv("YOUTUBE_API_KEY"))
|
||||
if apiResponse, err = PerformGetRequest(url); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
numVideos, _ := apiResponse.Int("pageInfo", "totalResults")
|
||||
if numVideos > 50 {
|
||||
numVideos = 50
|
||||
}
|
||||
|
||||
return jq, nil
|
||||
for i := 0; i < numVideos; i++ {
|
||||
index := strconv.Itoa(i)
|
||||
videoID, _ := apiResponse.String("items", index, "snippet", "resourceId", "videoId")
|
||||
if song, err := yt.NewSong(user, videoID, "", playlist); err == nil {
|
||||
songArray = append(songArray, song)
|
||||
}
|
||||
}
|
||||
return songArray, nil
|
||||
}
|
||||
|
|
|
@ -42,10 +42,12 @@ func (q *SongQueue) CurrentSong() Song {
|
|||
|
||||
// NextSong moves to the next Song in SongQueue. NextSong() removes the first Song in the queue.
|
||||
func (q *SongQueue) NextSong() {
|
||||
if q.CurrentSong().Playlist() != nil {
|
||||
if !isNil(q.CurrentSong().Playlist()) {
|
||||
if s, err := q.PeekNext(); err == nil {
|
||||
if s.Playlist() != nil && (q.CurrentSong().Playlist().ID() != s.Playlist().ID()) {
|
||||
q.CurrentSong().Playlist().DeleteSkippers()
|
||||
if !isNil(s.Playlist()) {
|
||||
if q.CurrentSong().Playlist().ID() != s.Playlist().ID() {
|
||||
q.CurrentSong().Playlist().DeleteSkippers()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
q.CurrentSong().Playlist().DeleteSkippers()
|
||||
|
|
16
strings.go
16
strings.go
|
@ -7,8 +7,8 @@
|
|||
|
||||
package main
|
||||
|
||||
// Message shown to users when the bot has an invalid YouTube API key.
|
||||
const INVALID_API_KEY = "MumbleDJ does not have a valid YouTube API key."
|
||||
// Message shown to users when the bot has an invalid API key.
|
||||
const INVALID_API_KEY = "MumbleDJ does not have a valid %s API key."
|
||||
|
||||
// Message shown to users when they do not have permission to execute a command.
|
||||
const NO_PERMISSION_MSG = "You do not have permission to execute that command."
|
||||
|
@ -26,7 +26,7 @@ const CHANNEL_DOES_NOT_EXIST_MSG = "The channel you specified does not exist."
|
|||
const INVALID_URL_MSG = "The URL you submitted does not match the required format."
|
||||
|
||||
// Message shown to users when they attempt to add a video that's too long
|
||||
const VIDEO_TOO_LONG_MSG = "The video 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."
|
||||
|
||||
// Message shown to users when they attempt to perform an action on a song when
|
||||
// no song is playing.
|
||||
|
@ -54,10 +54,10 @@ const ADMIN_SONG_SKIP_MSG = "An admin has decided to skip the current song."
|
|||
const ADMIN_PLAYLIST_SKIP_MSG = "An admin has decided to skip the current playlist."
|
||||
|
||||
// Message shown to users when the audio for a video could not be downloaded.
|
||||
const AUDIO_FAIL_MSG = "The audio download for this video failed. YouTube has likely not generated the audio files for this video yet. Skipping to the next song!"
|
||||
const AUDIO_FAIL_MSG = "The audio download for this video failed. %s has likely not generated the audio files for this %s yet. Skipping to the next song!"
|
||||
|
||||
// Message shown to users when they supply a YouTube URL that does not contain a valid ID.
|
||||
const INVALID_YOUTUBE_ID_MSG = "The YouTube URL you supplied did not contain a valid YouTube ID."
|
||||
// Message shown to users when they supply an URL that does not contain a valid ID.
|
||||
const INVALID_ID_MSG = "The %s URL you supplied did not contain a valid ID."
|
||||
|
||||
// Message shown to user when they successfully update the bot's comment.
|
||||
const COMMENT_UPDATED_MSG = "The comment for the bot has successfully been updated."
|
||||
|
@ -95,7 +95,7 @@ const PLAYLIST_SKIPPED_HTML = `
|
|||
const HELP_HTML = `<br/>
|
||||
<b>User Commands:</b>
|
||||
<p><b>!help</b> - Displays this help.</p>
|
||||
<p><b>!add</b> - Adds songs 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>!skip</b> - Casts a vote to skip the current song</p>
|
||||
<p> <b>!skipplaylist</b> - Casts a vote to skip over the current playlist.</p>
|
||||
|
@ -168,5 +168,5 @@ const CURRENT_SONG_HTML = `
|
|||
// Message shown to users when the currentsong command is issued when a song from a
|
||||
// playlist is playing.
|
||||
const CURRENT_SONG_PLAYLIST_HTML = `
|
||||
The song currently playing is "%s", added <b>%s</b> from the playlist "%s".
|
||||
The %s currently playing is "%s", added <b>%s</b> from the %s "%s".
|
||||
`
|
||||
|
|
276
youtube_dl.go
Normal file
276
youtube_dl.go
Normal file
|
@ -0,0 +1,276 @@
|
|||
/*
|
||||
* MumbleDJ
|
||||
* By Matthieu Grieger
|
||||
* youtube_dl.go
|
||||
* Copyright (c) 2014, 2015 Matthieu Grieger (MIT License)
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/jsonq"
|
||||
"github.com/layeh/gumble/gumble"
|
||||
"github.com/layeh/gumble/gumble_ffmpeg"
|
||||
)
|
||||
|
||||
// AudioTrack implements the Song interface
|
||||
type AudioTrack struct {
|
||||
id string
|
||||
title string
|
||||
thumbnail string
|
||||
submitter *gumble.User
|
||||
duration int
|
||||
url string
|
||||
offset int
|
||||
format string
|
||||
playlist Playlist
|
||||
skippers []string
|
||||
dontSkip bool
|
||||
service Service
|
||||
}
|
||||
|
||||
// AudioPlaylist implements the Playlist interface
|
||||
type AudioPlaylist struct {
|
||||
id string
|
||||
title string
|
||||
}
|
||||
|
||||
// ------------
|
||||
// YOUTUBEDL 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 (dl *AudioTrack) Download() error {
|
||||
|
||||
// Checks to see if song is already downloaded
|
||||
if _, err := os.Stat(fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, dl.Filename())); os.IsNotExist(err) {
|
||||
cmd := exec.Command("youtube-dl", "--verbose", "--no-mtime", "--output", fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, dl.Filename()), "--format", dl.format, "--prefer-ffmpeg", dl.url)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
if dj.conf.Cache.Enabled {
|
||||
dj.cache.CheckMaximumDirectorySize()
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
args := ""
|
||||
for s := range cmd.Args {
|
||||
args += cmd.Args[s] + " "
|
||||
}
|
||||
fmt.Printf(args + "\n" + string(output) + "\n" + "youtube-dl: " + err.Error() + "\n")
|
||||
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 song
|
||||
// thumbnail, URL, title, duration, and submitter.
|
||||
func (dl *AudioTrack) Play() {
|
||||
if dl.offset != 0 {
|
||||
offsetDuration, _ := time.ParseDuration(fmt.Sprintf("%ds", dl.offset))
|
||||
dj.audioStream.Offset = offsetDuration
|
||||
}
|
||||
dj.audioStream.Source = gumble_ffmpeg.SourceFile(fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, dl.Filename()))
|
||||
if err := dj.audioStream.Play(); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
message := `<table><tr><td align="center"><img src="%s" width=150 /></td></tr><tr><td align="center"><b><a href="%s">%s</a> (%s)</b></td></tr><tr><td align="center">Added by %s</td></tr>`
|
||||
message = fmt.Sprintf(message, dl.thumbnail, dl.url, dl.title, dl.Duration().String(), dl.submitter.Name)
|
||||
if !isNil(dl.playlist) {
|
||||
message = fmt.Sprintf(message+`<tr><td align="center">From playlist "%s"</td></tr>`, dl.Playlist().Title())
|
||||
}
|
||||
dj.client.Self.Channel.Send(message+`</table>`, false)
|
||||
|
||||
go func() {
|
||||
dj.audioStream.Wait()
|
||||
dj.queue.OnSongFinished()
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Delete deletes the song from ~/.mumbledj/songs if the cache is disabled.
|
||||
func (dl *AudioTrack) Delete() error {
|
||||
if dj.conf.Cache.Enabled == false {
|
||||
filePath := fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, dl.Filename())
|
||||
if _, err := os.Stat(filePath); err == nil {
|
||||
if err := os.Remove(filePath); err == nil {
|
||||
return nil
|
||||
}
|
||||
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 (dl *AudioTrack) AddSkip(username string) error {
|
||||
for _, user := range dl.skippers {
|
||||
if username == user {
|
||||
return errors.New("This user has already skipped the current song.")
|
||||
}
|
||||
}
|
||||
dl.skippers = append(dl.skippers, username)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveSkip removes a skip from the skippers slice. If username is not in slice, an error is
|
||||
// returned.
|
||||
func (dl *AudioTrack) RemoveSkip(username string) error {
|
||||
for i, user := range dl.skippers {
|
||||
if username == user {
|
||||
dl.skippers = append(dl.skippers[:i], dl.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 (dl *AudioTrack) SkipReached(channelUsers int) bool {
|
||||
if float32(len(dl.skippers))/float32(channelUsers) >= dj.conf.General.SkipRatio {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Submitter returns the name of the submitter of the Song.
|
||||
func (dl *AudioTrack) Submitter() string {
|
||||
return dl.submitter.Name
|
||||
}
|
||||
|
||||
// Title returns the title of the Song.
|
||||
func (dl *AudioTrack) Title() string {
|
||||
return dl.title
|
||||
}
|
||||
|
||||
// ID returns the id of the Song.
|
||||
func (dl *AudioTrack) ID() string {
|
||||
return dl.id
|
||||
}
|
||||
|
||||
// Filename returns the filename of the Song.
|
||||
func (dl *AudioTrack) Filename() string {
|
||||
return dl.id + "." + dl.format
|
||||
}
|
||||
|
||||
// Duration returns duration for the Song.
|
||||
func (dl *AudioTrack) Duration() time.Duration {
|
||||
timeDuration, _ := time.ParseDuration(strconv.Itoa(dl.duration) + "s")
|
||||
return timeDuration
|
||||
}
|
||||
|
||||
// Thumbnail returns the thumbnail URL for the Song.
|
||||
func (dl *AudioTrack) Thumbnail() string {
|
||||
return dl.thumbnail
|
||||
}
|
||||
|
||||
// Playlist returns the playlist type for the Song (may be nil).
|
||||
func (dl *AudioTrack) Playlist() Playlist {
|
||||
return dl.playlist
|
||||
}
|
||||
|
||||
// DontSkip returns the DontSkip boolean value for the Song.
|
||||
func (dl *AudioTrack) DontSkip() bool {
|
||||
return dl.dontSkip
|
||||
}
|
||||
|
||||
// SetDontSkip sets the DontSkip boolean value for the Song.
|
||||
func (dl *AudioTrack) SetDontSkip(value bool) {
|
||||
dl.dontSkip = value
|
||||
}
|
||||
|
||||
// ----------------
|
||||
// YOUTUBEDL PLAYLIST
|
||||
// ----------------
|
||||
|
||||
// AddSkip adds a skip to the playlist's skippers slice.
|
||||
func (p *AudioPlaylist) 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 *AudioPlaylist) 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 *AudioPlaylist) 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 *AudioPlaylist) 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 AudioPlaylist.
|
||||
func (p *AudioPlaylist) ID() string {
|
||||
return p.id
|
||||
}
|
||||
|
||||
// Title returns the title of the AudioPlaylist.
|
||||
func (p *AudioPlaylist) Title() string {
|
||||
return p.title
|
||||
}
|
||||
|
||||
// PerformGetRequest does all the grunt work for HTTPS GET request.
|
||||
func PerformGetRequest(url string) (*jsonq.JsonQuery, error) {
|
||||
jsonString := ""
|
||||
|
||||
if response, err := http.Get(url); err == nil {
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode == 200 {
|
||||
if body, err := ioutil.ReadAll(response.Body); err == nil {
|
||||
jsonString = string(body)
|
||||
}
|
||||
} else {
|
||||
if response.StatusCode == 403 {
|
||||
return nil, errors.New("Invalid API key supplied.")
|
||||
}
|
||||
return nil, errors.New("Invalid ID supplied.")
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("An error occurred while receiving HTTP GET response.")
|
||||
}
|
||||
|
||||
jsonData := map[string]interface{}{}
|
||||
decoder := json.NewDecoder(strings.NewReader(jsonString))
|
||||
decoder.Decode(&jsonData)
|
||||
jq := jsonq.NewQuery(jsonData)
|
||||
|
||||
return jq, nil
|
||||
}
|
Reference in a new issue