Merge branch 'dev'
Conflicts: .travis.yml README.md install-dependencies.sh service_youtube.go
This commit is contained in:
commit
6aeca636a4
19
.travis.yml
19
.travis.yml
|
@ -10,9 +10,26 @@ cache:
|
||||||
- $HOME/gopath/src/github.com/MichaelOultram/mumbledj/.vendor
|
- $HOME/gopath/src/github.com/MichaelOultram/mumbledj/.vendor
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
|
<<<<<<< HEAD
|
||||||
- export PATH=$PATH:~/bin/
|
- export PATH=$PATH:~/bin/
|
||||||
- export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:$HOME/opus/lib/pkgconfig
|
- export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:$HOME/opus/lib/pkgconfig
|
||||||
- bash install-dependencies.sh
|
- bash install-dependencies.sh
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- ls -R $HOME/opus
|
- ls -R $HOME/opus
|
||||||
|
=======
|
||||||
|
- export PATH=$PATH:$HOME/bin/
|
||||||
|
- export LD_RUN_PATH=$LD_RUN_PATH:$HOME/opus/lib
|
||||||
|
- export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$HOME/opus/lib
|
||||||
|
- export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:$HOME/opus/lib/pkgconfig
|
||||||
|
- bash install-dependencies.sh
|
||||||
|
|
||||||
|
script:
|
||||||
|
- make
|
||||||
|
- make install
|
||||||
|
|
||||||
|
after_success:
|
||||||
|
- ffmpeg -version
|
||||||
|
- youtube-dl --output ~/.mumbledj/songs/QcIy9NiNbmo.m4a --format m4a --prefer-ffmpeg -4 --verbose http://www.youtube.com/watch?v=QcIy9NiNbmo
|
||||||
|
- mumbledj -server=$MUMBLE_IP -port=$MUMBLE_PORT -username=travis -password=$MUMBLE_PASSWORD -verbose=true -test=true
|
||||||
|
>>>>>>> dev
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -13,7 +13,7 @@ install:
|
||||||
mkdir -p ~/.mumbledj/config
|
mkdir -p ~/.mumbledj/config
|
||||||
mkdir -p ~/.mumbledj/songs
|
mkdir -p ~/.mumbledj/songs
|
||||||
mkdir -p ~/.mumbledj/web
|
mkdir -p ~/.mumbledj/web
|
||||||
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
|
cp -u config.gcfg ~/.mumbledj/config/mumbledj.gcfg
|
||||||
cp -u index.html ~/.mumbledj/web/index.html
|
cp -u index.html ~/.mumbledj/web/index.html
|
||||||
if [ -d ~/bin ]; then cp -f mumbledj* ~/bin/mumbledj; else sudo cp -f mumbledj* /usr/local/bin/mumbledj; fi;
|
if [ -d ~/bin ]; then cp -f mumbledj* ~/bin/mumbledj; else sudo cp -f mumbledj* /usr/local/bin/mumbledj; fi;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
MumbleDJ [![Build Status](https://travis-ci.org/MichaelOultram/mumbledj.svg?branch=master)](https://travis-ci.org/MichaelOultram/mumbledj)
|
MumbleDJ [![Circle CI](https://circleci.com/gh/MichaelOultram/mumbledj/tree/master.svg?style=svg)](https://circleci.com/gh/MichaelOultram/mumbledj/tree/master)
|
||||||
========
|
========
|
||||||
**A Mumble bot that plays music fetched from YouTube videos.**
|
**A Mumble bot that plays music fetched from YouTube videos.**
|
||||||
|
|
||||||
|
|
32
circle.yml
Normal file
32
circle.yml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
machine:
|
||||||
|
environment:
|
||||||
|
PATH: $HOME/bin/:$PATH
|
||||||
|
LD_RUN_PATH: $LD_RUN_PATH:$HOME/opus/lib
|
||||||
|
LD_LIBRARY_PATH: $LD_LIBRARY_PATH:$HOME/opus/lib
|
||||||
|
PKG_CONFIG_PATH: $PKG_CONFIG_PATH:$HOME/opus/lib/pkgconfig
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
pre:
|
||||||
|
- ffmpeg -version
|
||||||
|
- sudo apt-get remove -y ffmpeg
|
||||||
|
- ffmpeg -version
|
||||||
|
- bash install-dependencies.sh
|
||||||
|
- ffmpeg -version
|
||||||
|
|
||||||
|
override:
|
||||||
|
- make
|
||||||
|
- make install
|
||||||
|
|
||||||
|
cache_directories:
|
||||||
|
- "~/opus"
|
||||||
|
- "~/bin"
|
||||||
|
- "~/mumbledj/.vendor"
|
||||||
|
- "/home/ubuntu/.go_workspace/bin"
|
||||||
|
- "/home/ubuntu/.go_workspace/pkg"
|
||||||
|
- "/home/ubuntu/.go_workspace/src/github.com/nitrous-io"
|
||||||
|
|
||||||
|
test:
|
||||||
|
override:
|
||||||
|
- youtube-dl --output ~/.mumbledj/songs/QcIy9NiNbmo.m4a --format m4a --prefer-ffmpeg -v http://www.youtube.com/watch?v=QcIy9NiNbmo
|
||||||
|
- mumbledj -server=$MUMBLE_IP -port=$MUMBLE_PORT -username=circleci -password=$MUMBLE_PASSWORD -verbose=true -test=true:
|
||||||
|
timeout: 180
|
13
commands.go
13
commands.go
|
@ -159,6 +159,13 @@ func parseCommand(user *gumble.User, username, command string) {
|
||||||
} else {
|
} else {
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
||||||
}
|
}
|
||||||
|
// Test command (WORKAROUND)
|
||||||
|
case "test":
|
||||||
|
if dj.HasPermission(username, dj.conf.Permissions.AdminKill) {
|
||||||
|
test.testYoutubeSong()
|
||||||
|
} else {
|
||||||
|
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
dj.SendPrivateMessage(user, COMMAND_DOESNT_EXIST_MSG)
|
dj.SendPrivateMessage(user, COMMAND_DOESNT_EXIST_MSG)
|
||||||
}
|
}
|
||||||
|
@ -171,7 +178,11 @@ func add(user *gumble.User, url string) error {
|
||||||
dj.SendPrivateMessage(user, NO_ARGUMENT_MSG)
|
dj.SendPrivateMessage(user, NO_ARGUMENT_MSG)
|
||||||
return errors.New("NO_ARGUMENT")
|
return errors.New("NO_ARGUMENT")
|
||||||
} else {
|
} else {
|
||||||
return findServiceAndAdd(user, url)
|
err := findServiceAndAdd(user, url)
|
||||||
|
if err != nil {
|
||||||
|
dj.SendPrivateMessage(user, err.Error())
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -144,7 +144,7 @@ AdminsEnabled = true
|
||||||
# SYNTAX: In order to specify multiple admins, repeat the Admins="username"
|
# SYNTAX: In order to specify multiple admins, repeat the Admins="username"
|
||||||
# line of code. Each line has one username, and an unlimited amount of usernames may
|
# line of code. Each line has one username, and an unlimited amount of usernames may
|
||||||
# be entered in this matter.
|
# be entered in this matter.
|
||||||
Admins = "Matt"
|
Admins = "BottleOToast"
|
||||||
|
|
||||||
# Make add an admin command?
|
# Make add an admin command?
|
||||||
# DEFAULT VALUE: false
|
# DEFAULT VALUE: false
|
||||||
|
|
75
index.html
75
index.html
|
@ -1,38 +1,49 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="ISO-8859-1">
|
<meta charset="ISO-8859-1">
|
||||||
<title>{{.User}} - mumbledj</title>
|
<title>{{.User}} - mumbledj</title>
|
||||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
|
<script
|
||||||
<script type="text/javascript">
|
src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
|
||||||
function api(type) {
|
<script type="text/javascript">
|
||||||
return "http://{{.Site}}/" + type + "?token={{.Token}}";
|
function onLoad() {
|
||||||
}
|
window.setInterval(function() {
|
||||||
|
// Get the song queue
|
||||||
|
}, 6000);
|
||||||
|
}
|
||||||
|
|
||||||
function addURL() {
|
function setAPI(type, val) {
|
||||||
var url = $("#textbox");
|
$.ajax({
|
||||||
$.ajax(api("add") + "&value=" + url.attr("value"));
|
url : "http://{{.Site}}/api/" + type + "?token={{.Token}}"
|
||||||
url.attr("value", "");
|
+ "&value=" + val,
|
||||||
}
|
complete : apiComplete,
|
||||||
|
cache : false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function volume() {
|
function apiComplete(jqXHR, textStatus) {
|
||||||
var volume = $("#textbox");
|
alert(textStatus);
|
||||||
$.ajax(api("volume") + "&value=" + volume.attr("value"));
|
}
|
||||||
volume.attr("value", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
function skip(val) {
|
function txtBox(type) {
|
||||||
$.ajax(api("skip") + "&value=" + val);
|
var txt = $("#textbox");
|
||||||
}
|
api(type, txt.attr("value"));
|
||||||
|
txt.attr("value", "");
|
||||||
</script>
|
}
|
||||||
</head>
|
</script>
|
||||||
<body>
|
</head>
|
||||||
<h1>Add Song Form</h1>
|
<body onload="onLoad();" bgcolor="#FFFFFF">
|
||||||
<input id="textbox" type="text"/>
|
<h1>Add Song Form</h1>
|
||||||
<input id="add" type="button" value="Add Song" onclick="addURL()"/>
|
<input id="textbox" type="text" />
|
||||||
<input id="volume" type="button" value="Set Volume" onclick="volume()"/>
|
<input id="add" type="button" value="Add Song"
|
||||||
<input id="skipSong" type="button" value="Skip Current Song" onclick="skip('false')"/>
|
onclick="setAPI('add', $('#textbox').attr('value'))" />
|
||||||
<input id="skipPlaylist" type="button" value="Skip Current Playlist" onclick="skip('true')"/>
|
<input id="volume" type="button" value="Set Volume"
|
||||||
</body>
|
onclick="setAPI('volume', $('#textbox').attr('value'))" />
|
||||||
|
<input id="skipSong" type="button" value="Skip Current Song"
|
||||||
|
onclick="setAPI('skip', false)" />
|
||||||
|
<input id="skipPlaylist" type="button" value="Skip Current Playlist"
|
||||||
|
onclick="setAPI('skip', true)" />
|
||||||
|
<br />
|
||||||
|
<textarea id="status" rows="10" cols="30"></textarea>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -3,16 +3,18 @@ set -e
|
||||||
|
|
||||||
# check to see if ffmpeg is installed
|
# check to see if ffmpeg is installed
|
||||||
if [ ! -f "$HOME/bin/ffmpeg" ]; then
|
if [ ! -f "$HOME/bin/ffmpeg" ]; then
|
||||||
|
echo 'Installing ffmpeg'
|
||||||
wget http://johnvansickle.com/ffmpeg/releases/ffmpeg-release-64bit-static.tar.xz -O /tmp/ffmpeg.tar.xz
|
wget http://johnvansickle.com/ffmpeg/releases/ffmpeg-release-64bit-static.tar.xz -O /tmp/ffmpeg.tar.xz
|
||||||
tar -xvf /tmp/ffmpeg.tar.xz --strip 1 --no-anchored ffmpeg ffprobe
|
tar -xvf /tmp/ffmpeg.tar.xz --strip 1 --no-anchored ffmpeg ffprobe
|
||||||
chmod a+rx ffmpeg ffprobe
|
chmod a+rx ffmpeg ffprobe
|
||||||
mv ff* $HOME/bin
|
mv ff* ~/bin
|
||||||
else
|
else
|
||||||
echo 'Using cached version of ffmpeg.';
|
echo 'Using cached version of ffmpeg.';
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# check to see if opus is installed
|
# check to see if opus is installed
|
||||||
if [ ! -d "$HOME/opus/lib" ]; then
|
if [ ! -d "$HOME/opus/lib" ]; then
|
||||||
|
echo 'Installing opus'
|
||||||
wget http://downloads.xiph.org/releases/opus/opus-1.0.3.tar.gz
|
wget http://downloads.xiph.org/releases/opus/opus-1.0.3.tar.gz
|
||||||
tar xzvf opus-1.0.3.tar.gz
|
tar xzvf opus-1.0.3.tar.gz
|
||||||
cd opus-1.0.3 && ./configure --prefix=$HOME/opus && make && make install
|
cd opus-1.0.3 && ./configure --prefix=$HOME/opus && make && make install
|
||||||
|
@ -22,6 +24,7 @@ fi
|
||||||
|
|
||||||
# check to see if youtube-dl is installed
|
# check to see if youtube-dl is installed
|
||||||
if [ ! -f "$HOME/bin/youtube-dl" ]; then
|
if [ ! -f "$HOME/bin/youtube-dl" ]; then
|
||||||
|
echo 'Installing youtube-dl'
|
||||||
curl https://yt-dl.org/downloads/2015.07.28/youtube-dl -o ~/bin/youtube-dl
|
curl https://yt-dl.org/downloads/2015.07.28/youtube-dl -o ~/bin/youtube-dl
|
||||||
chmod a+rx ~/bin/youtube-dl
|
chmod a+rx ~/bin/youtube-dl
|
||||||
else
|
else
|
||||||
|
|
12
main.go
12
main.go
|
@ -142,12 +142,14 @@ func PerformStartupChecks() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prints out messages only if verbose flag is true
|
||||||
func Verbose(msg string) {
|
func Verbose(msg string) {
|
||||||
if dj.verbose {
|
if dj.verbose {
|
||||||
fmt.Printf(msg + "\n")
|
fmt.Printf(msg + "\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks to see if an object is nil
|
||||||
func isNil(a interface{}) bool {
|
func isNil(a interface{}) bool {
|
||||||
defer func() { recover() }()
|
defer func() { recover() }()
|
||||||
return a == nil || reflect.ValueOf(a).IsNil()
|
return a == nil || reflect.ValueOf(a).IsNil()
|
||||||
|
@ -191,7 +193,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
var address, port, username, password, channel, pemCert, pemKey, accesstokens string
|
var address, port, username, password, channel, pemCert, pemKey, accesstokens string
|
||||||
var insecure, verbose bool
|
var insecure, verbose, testcode bool
|
||||||
|
|
||||||
flag.StringVar(&address, "server", "localhost", "address for Mumble server")
|
flag.StringVar(&address, "server", "localhost", "address for Mumble server")
|
||||||
flag.StringVar(&port, "port", "64738", "port for Mumble server")
|
flag.StringVar(&port, "port", "64738", "port for Mumble server")
|
||||||
|
@ -202,7 +204,8 @@ func main() {
|
||||||
flag.StringVar(&pemKey, "key", "", "path to user PEM key for MumbleDJ")
|
flag.StringVar(&pemKey, "key", "", "path to user PEM key for MumbleDJ")
|
||||||
flag.StringVar(&accesstokens, "accesstokens", "", "list of access tokens for channel auth")
|
flag.StringVar(&accesstokens, "accesstokens", "", "list of access tokens for channel auth")
|
||||||
flag.BoolVar(&insecure, "insecure", false, "skip certificate checking")
|
flag.BoolVar(&insecure, "insecure", false, "skip certificate checking")
|
||||||
flag.BoolVar(&verbose, "verbose", false, "prints out debug messages to the console")
|
flag.BoolVar(&verbose, "verbose", false, "[debug] prints out debug messages to the console")
|
||||||
|
flag.BoolVar(&testcode, "test", false, "[debug] tests the features of mumbledj")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
dj.config = gumble.Config{
|
dj.config = gumble.Config{
|
||||||
|
@ -244,6 +247,11 @@ func main() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if testcode {
|
||||||
|
Verbose("Testing is enabled")
|
||||||
|
Test(password, address, port, strings.Split(accesstokens, " "))
|
||||||
|
}
|
||||||
|
|
||||||
web = NewWebServer(9563)
|
web = NewWebServer(9563)
|
||||||
web.makeWeb()
|
web.makeWeb()
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ type Playlist interface {
|
||||||
Title() string
|
Title() string
|
||||||
}
|
}
|
||||||
|
|
||||||
var services = []Service{YouTube{}}
|
var services = []Service{YouTube{}, SoundCloud{}}
|
||||||
|
|
||||||
func findServiceAndAdd(user *gumble.User, url string) error {
|
func findServiceAndAdd(user *gumble.User, url string) error {
|
||||||
var urlService Service
|
var urlService Service
|
||||||
|
@ -65,6 +65,7 @@ func findServiceAndAdd(user *gumble.User, url string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if urlService == nil {
|
if urlService == nil {
|
||||||
|
Verbose("Invalid_URL")
|
||||||
return errors.New("INVALID_URL")
|
return errors.New("INVALID_URL")
|
||||||
} else {
|
} else {
|
||||||
oldLength := dj.queue.Len()
|
oldLength := dj.queue.Len()
|
||||||
|
|
126
service_soundcloud.go
Normal file
126
service_soundcloud.go
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/jmoiron/jsonq"
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Regular expressions for soundcloud urls
|
||||||
|
var soundcloudSongPattern = `https?:\/\/(www\.)?soundcloud\.com\/([\w-]+)\/([\w-]+)`
|
||||||
|
var soundcloudPlaylistPattern = `https?:\/\/(www\.)?soundcloud\.com\/([\w-]+)\/sets\/([\w-]+)`
|
||||||
|
|
||||||
|
// SoundCloud implements the Service interface
|
||||||
|
type SoundCloud struct{}
|
||||||
|
|
||||||
|
// ------------------
|
||||||
|
// SOUNDCLOUD SERVICE
|
||||||
|
// ------------------
|
||||||
|
|
||||||
|
// Name of the service
|
||||||
|
func (sc SoundCloud) ServiceName() string {
|
||||||
|
return "SoundCloud"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks to see if service will accept URL
|
||||||
|
func (sc SoundCloud) URLRegex(url string) bool {
|
||||||
|
return RegexpFromURL(url, []string{soundcloudSongPattern, soundcloudPlaylistPattern}) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates the requested song/playlist and adds to the queue
|
||||||
|
func (sc SoundCloud) NewRequest(user *gumble.User, url string) (string, error) {
|
||||||
|
var apiResponse *jsonq.JsonQuery
|
||||||
|
var err error
|
||||||
|
url = fmt.Sprintf("http://api.soundcloud.com/resolve?url=%s&client_id=%s", url, os.Getenv("SOUNDCLOUD_API_KEY"))
|
||||||
|
if apiResponse, err = PerformGetRequest(url); err != nil {
|
||||||
|
return "", errors.New(INVALID_API_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
tracks, err := apiResponse.ArrayOfObjects("tracks")
|
||||||
|
if err == nil {
|
||||||
|
// PLAYLIST
|
||||||
|
if dj.HasPermission(user.Name, dj.conf.Permissions.AdminAddPlaylists) {
|
||||||
|
// Check duration of playlist
|
||||||
|
//duration, _ := apiResponse.Int("duration")
|
||||||
|
|
||||||
|
// Create playlist
|
||||||
|
title, _ := apiResponse.String("title")
|
||||||
|
permalink, _ := apiResponse.String("permalink_url")
|
||||||
|
playlist := &YouTubeDLPlaylist{
|
||||||
|
id: permalink,
|
||||||
|
title: title,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all tracks
|
||||||
|
for _, t := range tracks {
|
||||||
|
sc.NewSong(user, jsonq.NewQuery(t), playlist)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
return playlist.Title(), nil
|
||||||
|
} else {
|
||||||
|
Verbose("soundcloud.NewRequest: " + err.Error())
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "", errors.New("NO_PLAYLIST_PERMISSION")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// SONG
|
||||||
|
return sc.NewSong(user, apiResponse, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a track and adds to the queue
|
||||||
|
func (sc SoundCloud) NewSong(user *gumble.User, trackData *jsonq.JsonQuery, playlist Playlist) (string, error) {
|
||||||
|
title, err := trackData.String("title")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := trackData.Int("id")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
duration, err := trackData.Int("duration")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
thumbnail, err := trackData.String("artwork_url")
|
||||||
|
if err != nil {
|
||||||
|
// Song has no artwork, using profile avatar instead
|
||||||
|
userObj, err := trackData.Object("user")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
thumbnail, err = jsonq.NewQuery(userObj).String("avatar_url")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
url, err := trackData.String("permalink_url")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
song := &YouTubeDLSong{
|
||||||
|
id: strconv.Itoa(id),
|
||||||
|
title: title,
|
||||||
|
url: url,
|
||||||
|
thumbnail: thumbnail,
|
||||||
|
submitter: user,
|
||||||
|
duration: strconv.Itoa(duration),
|
||||||
|
format: "mp3",
|
||||||
|
playlist: playlist,
|
||||||
|
skippers: make([]string, 0),
|
||||||
|
dontSkip: false,
|
||||||
|
}
|
||||||
|
dj.queue.AddSong(song)
|
||||||
|
return title, nil
|
||||||
|
}
|
|
@ -14,15 +14,12 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jmoiron/jsonq"
|
"github.com/jmoiron/jsonq"
|
||||||
"github.com/layeh/gumble/gumble"
|
"github.com/layeh/gumble/gumble"
|
||||||
"github.com/layeh/gumble/gumble_ffmpeg"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Regular expressions for youtube urls
|
// Regular expressions for youtube urls
|
||||||
|
@ -42,26 +39,6 @@ var youtubeVideoPatterns = []string{
|
||||||
// YouTube implements the Service interface
|
// YouTube implements the Service interface
|
||||||
type YouTube struct{}
|
type YouTube struct{}
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// YouTubePlaylist holds the metadata for a YouTube playlist.
|
|
||||||
type YouTubePlaylist struct {
|
|
||||||
id string
|
|
||||||
title string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------
|
// ---------------
|
||||||
// YOUTUBE SERVICE
|
// YOUTUBE SERVICE
|
||||||
// ---------------
|
// ---------------
|
||||||
|
@ -83,7 +60,7 @@ func (yt YouTube) NewRequest(user *gumble.User, url string) (string, error) {
|
||||||
if re.MatchString(url) {
|
if re.MatchString(url) {
|
||||||
if dj.HasPermission(user.Name, dj.conf.Permissions.AdminAddPlaylists) {
|
if dj.HasPermission(user.Name, dj.conf.Permissions.AdminAddPlaylists) {
|
||||||
shortURL = re.FindStringSubmatch(url)[1]
|
shortURL = re.FindStringSubmatch(url)[1]
|
||||||
playlist, err := yt.NewPlaylist(user.Name, shortURL)
|
playlist, err := yt.NewPlaylist(user, shortURL)
|
||||||
return playlist.Title(), err
|
return playlist.Title(), err
|
||||||
} else {
|
} else {
|
||||||
return "", errors.New("NO_PLAYLIST_PERMISSION")
|
return "", errors.New("NO_PLAYLIST_PERMISSION")
|
||||||
|
@ -95,10 +72,11 @@ func (yt YouTube) NewRequest(user *gumble.User, url string) (string, error) {
|
||||||
if len(matches[0]) == 3 {
|
if len(matches[0]) == 3 {
|
||||||
startOffset = matches[0][2]
|
startOffset = matches[0][2]
|
||||||
}
|
}
|
||||||
song, err := yt.NewSong(user.Name, shortURL, startOffset, nil)
|
song, err := yt.NewSong(user, shortURL, startOffset, nil)
|
||||||
if !isNil(song) {
|
if !isNil(song) {
|
||||||
return song.Title(), err
|
return song.Title(), nil
|
||||||
} else {
|
} else {
|
||||||
|
Verbose("youtube.NewRequest: " + err.Error())
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,12 +87,12 @@ func (yt YouTube) NewRequest(user *gumble.User, url string) (string, error) {
|
||||||
|
|
||||||
// NewSong gathers the metadata for a song extracted from a YouTube video, and returns
|
// NewSong gathers the metadata for a song extracted from a YouTube video, and returns
|
||||||
// the song.
|
// the song.
|
||||||
func (yt YouTube) NewSong(user, id, offset string, playlist *YouTubePlaylist) (*YouTubeSong, error) {
|
func (yt YouTube) NewSong(user *gumble.User, id, offset string, playlist Playlist) (Song, error) {
|
||||||
var apiResponse *jsonq.JsonQuery
|
var apiResponse *jsonq.JsonQuery
|
||||||
var err error
|
var err error
|
||||||
url := fmt.Sprintf("https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails&id=%s&key=%s",
|
url := fmt.Sprintf("https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails&id=%s&key=%s",
|
||||||
id, os.Getenv("YOUTUBE_API_KEY"))
|
id, os.Getenv("YOUTUBE_API_KEY"))
|
||||||
if apiResponse, err = yt.PerformGetRequest(url); err != nil {
|
if apiResponse, err = PerformGetRequest(url); err != nil {
|
||||||
return nil, errors.New(INVALID_API_KEY)
|
return nil, errors.New(INVALID_API_KEY)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,14 +161,15 @@ func (yt YouTube) NewSong(user, id, offset string, playlist *YouTubePlaylist) (*
|
||||||
}
|
}
|
||||||
|
|
||||||
if dj.conf.General.MaxSongDuration == 0 || totalSeconds <= dj.conf.General.MaxSongDuration {
|
if dj.conf.General.MaxSongDuration == 0 || totalSeconds <= dj.conf.General.MaxSongDuration {
|
||||||
song := &YouTubeSong{
|
song := &YouTubeDLSong{
|
||||||
submitter: user,
|
submitter: user,
|
||||||
title: title,
|
title: title,
|
||||||
id: id,
|
id: id,
|
||||||
|
url: "https://youtu.be/" + id,
|
||||||
offset: int((offsetDays * 86400) + (offsetHours * 3600) + (offsetMinutes * 60) + offsetSeconds),
|
offset: int((offsetDays * 86400) + (offsetHours * 3600) + (offsetMinutes * 60) + offsetSeconds),
|
||||||
filename: id + ".m4a",
|
|
||||||
duration: durationString,
|
duration: durationString,
|
||||||
thumbnail: thumbnail,
|
thumbnail: thumbnail,
|
||||||
|
format: "m4a",
|
||||||
skippers: make([]string, 0),
|
skippers: make([]string, 0),
|
||||||
playlist: playlist,
|
playlist: playlist,
|
||||||
dontSkip: false,
|
dontSkip: false,
|
||||||
|
@ -204,18 +183,17 @@ func (yt YouTube) NewSong(user, id, offset string, playlist *YouTubePlaylist) (*
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPlaylist gathers the metadata for a YouTube playlist and returns it.
|
// NewPlaylist gathers the metadata for a YouTube playlist and returns it.
|
||||||
func (yt YouTube) NewPlaylist(user, id string) (*YouTubePlaylist, error) {
|
func (yt YouTube) NewPlaylist(user *gumble.User, id string) (Playlist, error) {
|
||||||
var apiResponse *jsonq.JsonQuery
|
var apiResponse *jsonq.JsonQuery
|
||||||
var err error
|
var err error
|
||||||
// Retrieve title of playlist
|
// Retrieve title of playlist
|
||||||
url := fmt.Sprintf("https://www.googleapis.com/youtube/v3/playlists?part=snippet&id=%s&key=%s",
|
url := fmt.Sprintf("https://www.googleapis.com/youtube/v3/playlists?part=snippet&id=%s&key=%s", id, os.Getenv("YOUTUBE_API_KEY"))
|
||||||
id, os.Getenv("YOUTUBE_API_KEY"))
|
if apiResponse, err = PerformGetRequest(url); err != nil {
|
||||||
if apiResponse, err = yt.PerformGetRequest(url); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
title, _ := apiResponse.String("items", "0", "snippet", "title")
|
title, _ := apiResponse.String("items", "0", "snippet", "title")
|
||||||
|
|
||||||
playlist := &YouTubePlaylist{
|
playlist := &YouTubeDLPlaylist{
|
||||||
id: id,
|
id: id,
|
||||||
title: title,
|
title: title,
|
||||||
}
|
}
|
||||||
|
@ -223,7 +201,7 @@ func (yt YouTube) NewPlaylist(user, id string) (*YouTubePlaylist, error) {
|
||||||
// Retrieve items in playlist
|
// Retrieve items in playlist
|
||||||
url = fmt.Sprintf("https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=50&playlistId=%s&key=%s",
|
url = fmt.Sprintf("https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=50&playlistId=%s&key=%s",
|
||||||
id, os.Getenv("YOUTUBE_API_KEY"))
|
id, os.Getenv("YOUTUBE_API_KEY"))
|
||||||
if apiResponse, err = yt.PerformGetRequest(url); err != nil {
|
if apiResponse, err = PerformGetRequest(url); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
numVideos, _ := apiResponse.Int("pageInfo", "totalResults")
|
numVideos, _ := apiResponse.Int("pageInfo", "totalResults")
|
||||||
|
@ -239,241 +217,8 @@ func (yt YouTube) NewPlaylist(user, id string) (*YouTubePlaylist, error) {
|
||||||
return playlist, nil
|
return playlist, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------
|
|
||||||
// YOUTUBE 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 (s *YouTubeSong) Download() error {
|
|
||||||
|
|
||||||
// Checks to see if song is already downloaded
|
|
||||||
if _, err := os.Stat(fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, s.Filename())); os.IsNotExist(err) {
|
|
||||||
Verbose("Downloading " + s.Title())
|
|
||||||
cmd := exec.Command("youtube-dl", "--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()
|
|
||||||
}
|
|
||||||
Verbose(s.Title() + " downloaded")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
Verbose(s.Title() + " failed to download")
|
|
||||||
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 isNil(s.Playlist()) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
Verbose("Now playing " + s.Title())
|
|
||||||
|
|
||||||
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 {
|
|
||||||
Verbose("Deleted " + s.Title())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
Verbose("Failed to delete " + s.Title())
|
|
||||||
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
|
|
||||||
// ----------------
|
|
||||||
|
|
||||||
// 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.
|
// PerformGetRequest does all the grunt work for a YouTube HTTPS GET request.
|
||||||
func (yt YouTube) PerformGetRequest(url string) (*jsonq.JsonQuery, error) {
|
func PerformGetRequest(url string) (*jsonq.JsonQuery, error) {
|
||||||
jsonString := ""
|
jsonString := ""
|
||||||
|
|
||||||
if response, err := http.Get(url); err == nil {
|
if response, err := http.Get(url); err == nil {
|
||||||
|
@ -486,7 +231,7 @@ func (yt YouTube) PerformGetRequest(url string) (*jsonq.JsonQuery, error) {
|
||||||
if response.StatusCode == 403 {
|
if response.StatusCode == 403 {
|
||||||
return nil, errors.New("Invalid API key supplied.")
|
return nil, errors.New("Invalid API key supplied.")
|
||||||
}
|
}
|
||||||
return nil, errors.New("Invalid YouTube ID supplied.")
|
return nil, errors.New("Invalid ID supplied.")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return nil, errors.New("An error occurred while receiving HTTP GET response.")
|
return nil, errors.New("An error occurred while receiving HTTP GET response.")
|
||||||
|
|
|
@ -77,6 +77,14 @@ func (q *SongQueue) Traverse(visit func(i int, s Song)) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gets the song at a specific point in the queue
|
||||||
|
func (q *SongQueue) Get(i int) (Song, error) {
|
||||||
|
if q.Len() > i+1 {
|
||||||
|
return q.queue[i], nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("Out of Bounds")
|
||||||
|
}
|
||||||
|
|
||||||
// OnSongFinished event. Deletes Song that just finished playing, then queues the next Song (if exists).
|
// OnSongFinished event. Deletes Song that just finished playing, then queues the next Song (if exists).
|
||||||
func (q *SongQueue) OnSongFinished() {
|
func (q *SongQueue) OnSongFinished() {
|
||||||
resetOffset, _ := time.ParseDuration(fmt.Sprintf("%ds", 0))
|
resetOffset, _ := time.ParseDuration(fmt.Sprintf("%ds", 0))
|
||||||
|
|
85
test.go
Normal file
85
test.go
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/layeh/gumble/gumbleutil"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestSettings struct {
|
||||||
|
password string
|
||||||
|
ip string
|
||||||
|
port string
|
||||||
|
accesstokens []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var test TestSettings
|
||||||
|
|
||||||
|
func Test(password, ip, port string, accesstokens []string) {
|
||||||
|
test = TestSettings{
|
||||||
|
password: password,
|
||||||
|
ip: ip,
|
||||||
|
port: port,
|
||||||
|
accesstokens: accesstokens,
|
||||||
|
}
|
||||||
|
test.testYoutubeSong()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TestSettings) createClient(uname string) *gumble.Client {
|
||||||
|
client := gumble.NewClient(&gumble.Config{
|
||||||
|
Username: uname,
|
||||||
|
Password: t.password,
|
||||||
|
Address: t.ip + ":" + t.port,
|
||||||
|
Tokens: t.accesstokens,
|
||||||
|
})
|
||||||
|
gumbleutil.CertificateLockFile(client, fmt.Sprintf("%s/.mumbledj/cert.lock", dj.homeDir))
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TestSettings) testYoutubeSong() {
|
||||||
|
// dummyClient := t.createClient("dummy")
|
||||||
|
// if err := dummyClient.Connect(); err != nil {
|
||||||
|
// panic(err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
dj.client.Request(gumble.RequestUserList)
|
||||||
|
time.Sleep(time.Second * 5)
|
||||||
|
dummyUser := dj.client.Users.Find("BottleOToast")
|
||||||
|
if dummyUser == nil {
|
||||||
|
fmt.Printf("User does not exist, printing users\n")
|
||||||
|
for _, user := range dj.client.Users {
|
||||||
|
fmt.Printf(user.Name + "\n")
|
||||||
|
}
|
||||||
|
fmt.Printf("End of user list\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't judge, I used the (autogenerated) Top Tracks for United Kingdom playlist
|
||||||
|
songs := map[string]string{
|
||||||
|
"http://www.youtube.com/watch?v=QcIy9NiNbmo": "Taylor Swift - Bad Blood ft. Kendrick Lamar",
|
||||||
|
"https://www.youtube.com/watch?v=vjW8wmF5VWc": "Silentó - Watch Me (Whip/Nae Nae) (Official)",
|
||||||
|
"http://youtu.be/nsDwItoNlLc": "Tinie Tempah ft. Jess Glynne - Not Letting Go (Official Video)",
|
||||||
|
"https://youtu.be/hXTAn4ELEwM": "Years & Years - Shine",
|
||||||
|
"http://youtube.com/watch?v=RgKAFK5djSk": "Wiz Khalifa - See You Again ft. Charlie Puth [Official Video] Furious 7 Soundtrack",
|
||||||
|
"https://youtube.com/watch?v=qWWSM3wCiKY": "Calvin Harris & Disciples - How Deep Is Your Love (Audio)",
|
||||||
|
"http://www.youtube.com/v/yzTuBuRdAyA": "The Weeknd - The Hills",
|
||||||
|
"https://www.youtube.com/v/cNw8A5pwbVI": "Pia Mia - Do It Again ft. Chris Brown, Tyga",
|
||||||
|
}
|
||||||
|
|
||||||
|
for url, title := range songs {
|
||||||
|
err := add(dummyUser, url)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("For: %s; Expected: %s; Got: %s\n", url, title, err.Error())
|
||||||
|
} else if dj.queue.CurrentSong().Title() != title {
|
||||||
|
fmt.Printf("For: %s; Expected: %s; Got: %s\n", url, title, dj.queue.CurrentSong().Title())
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second * 5)
|
||||||
|
skip(dummyUser, false, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(0)
|
||||||
|
//dummyClient.Disconnect()
|
||||||
|
}
|
65
web.go
65
web.go
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
//"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
@ -8,7 +9,6 @@ import (
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -28,6 +28,21 @@ type Page struct {
|
||||||
User string
|
User string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Status struct {
|
||||||
|
Error bool
|
||||||
|
ErrorMsg string
|
||||||
|
Queue []SongInfo
|
||||||
|
}
|
||||||
|
type SongInfo struct {
|
||||||
|
TitleID string
|
||||||
|
PlaylistID string
|
||||||
|
Title string
|
||||||
|
Playlist string
|
||||||
|
Submitter string
|
||||||
|
Duration string
|
||||||
|
Thumbnail string
|
||||||
|
}
|
||||||
|
|
||||||
var external_ip = ""
|
var external_ip = ""
|
||||||
|
|
||||||
func NewWebServer(port int) *WebServer {
|
func NewWebServer(port int) *WebServer {
|
||||||
|
@ -41,9 +56,10 @@ func NewWebServer(port int) *WebServer {
|
||||||
|
|
||||||
func (web *WebServer) makeWeb() {
|
func (web *WebServer) makeWeb() {
|
||||||
http.HandleFunc("/", web.homepage)
|
http.HandleFunc("/", web.homepage)
|
||||||
http.HandleFunc("/add", web.add)
|
http.HandleFunc("/api/add", web.add)
|
||||||
http.HandleFunc("/volume", web.volume)
|
http.HandleFunc("/api/volume", web.volume)
|
||||||
http.HandleFunc("/skip", web.skip)
|
http.HandleFunc("/api/skip", web.skip)
|
||||||
|
//http.HandleFunc("/api/status", web.status)
|
||||||
http.ListenAndServe(":"+strconv.Itoa(web.port), nil)
|
http.ListenAndServe(":"+strconv.Itoa(web.port), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,8 +68,14 @@ func (web *WebServer) homepage(w http.ResponseWriter, r *http.Request) {
|
||||||
if uname == nil {
|
if uname == nil {
|
||||||
fmt.Fprintf(w, "Invalid Token")
|
fmt.Fprintf(w, "Invalid Token")
|
||||||
} else {
|
} else {
|
||||||
cwd, _ := os.Getwd()
|
var webpage = uname.Name
|
||||||
t, err := template.ParseFiles(filepath.Join(cwd, "./.mumbledj/web/index.html"))
|
|
||||||
|
// Check to see if user has a custom webpage
|
||||||
|
if _, err := os.Stat(fmt.Sprintf("%s/.mumbledj/web/%s.html", dj.homeDir, uname.Name)); os.IsNotExist(err) {
|
||||||
|
webpage = "index"
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := template.ParseFiles(fmt.Sprintf("%s/.mumbledj/web/%s.html", dj.homeDir, webpage))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
@ -103,6 +125,35 @@ func (web *WebServer) skip(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//func (web *WebServer) status(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// var uname = web.token_client[r.FormValue("token")]
|
||||||
|
// if uname == nil {
|
||||||
|
// str, ok := json.Marshal(&Status{true, "Invalid Token"}).(string)
|
||||||
|
// fmt.Fprintf(w, str)
|
||||||
|
// } else {
|
||||||
|
// // Generate song queue
|
||||||
|
// queueLength := dj.queue.Len()
|
||||||
|
// var songsInQueue [queueLength]SongInfo
|
||||||
|
// for i := 0; i < dj.queue.Len(); i++ {
|
||||||
|
// songItem := dj.queue.Get(i)
|
||||||
|
// songsInQueue[i] = &SongInfo{
|
||||||
|
// TitleID: songItem.ID(),
|
||||||
|
// Title: songItem.Title(),
|
||||||
|
// Submitter: songItem.Submitter(),
|
||||||
|
// Duration: songItem.Duration(),
|
||||||
|
// Thumbnail: songItem.Thumbnail(),
|
||||||
|
// }
|
||||||
|
// if !isNil(songItem.Playlist()) {
|
||||||
|
// songsInQueue[i].PlaylistID = songItem.Playlist().ID()
|
||||||
|
// songsInQueue[i].Playlist = songItem.Playlist().Title()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Output status
|
||||||
|
// fmt.Fprintf(w, string(json.MarshalIndent(&Status{false, "", songsInQueue})))
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
func (website *WebServer) GetWebAddress(user *gumble.User) {
|
func (website *WebServer) GetWebAddress(user *gumble.User) {
|
||||||
Verbose("Port number: " + strconv.Itoa(web.port))
|
Verbose("Port number: " + strconv.Itoa(web.port))
|
||||||
if web.client_token[user] != "" {
|
if web.client_token[user] != "" {
|
||||||
|
@ -110,7 +161,7 @@ func (website *WebServer) GetWebAddress(user *gumble.User) {
|
||||||
}
|
}
|
||||||
// dealing with collisions
|
// dealing with collisions
|
||||||
var firstLoop = true
|
var firstLoop = true
|
||||||
for firstLoop || web.token_client[web.client_token[user]] != nil {
|
for firstLoop || web.token_client[web.client_token[user]] != nil || web.client_token[user] == "api" {
|
||||||
web.client_token[user] = randSeq(10)
|
web.client_token[user] = randSeq(10)
|
||||||
firstLoop = false
|
firstLoop = false
|
||||||
}
|
}
|
||||||
|
|
235
youtube_dl.go
Normal file
235
youtube_dl.go
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/layeh/gumble/gumble_ffmpeg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Extends a Song
|
||||||
|
type YouTubeDLSong struct {
|
||||||
|
id string
|
||||||
|
title string
|
||||||
|
thumbnail string
|
||||||
|
submitter *gumble.User
|
||||||
|
duration string
|
||||||
|
url string
|
||||||
|
offset int
|
||||||
|
format string
|
||||||
|
playlist Playlist
|
||||||
|
skippers []string
|
||||||
|
dontSkip bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type YouTubeDLPlaylist struct {
|
||||||
|
id string
|
||||||
|
title string
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------
|
||||||
|
// YouTubeDLSong
|
||||||
|
// -------------
|
||||||
|
|
||||||
|
// 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 *YouTubeDLSong) 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", "--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] + " "
|
||||||
|
}
|
||||||
|
Verbose(args)
|
||||||
|
Verbose(string(output))
|
||||||
|
Verbose("youtube-dl: " + err.Error())
|
||||||
|
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 *YouTubeDLSong) 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, 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)
|
||||||
|
Verbose("Now playing " + dl.title)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
dj.audioStream.Wait()
|
||||||
|
dj.queue.OnSongFinished()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes the song from ~/.mumbledj/songs if the cache is disabled.
|
||||||
|
func (dl *YouTubeDLSong) 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 *YouTubeDLSong) 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 *YouTubeDLSong) 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 *YouTubeDLSong) 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 *YouTubeDLSong) Submitter() string {
|
||||||
|
return dl.submitter.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title returns the title of the Song.
|
||||||
|
func (dl *YouTubeDLSong) Title() string {
|
||||||
|
return dl.title
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns the id of the Song.
|
||||||
|
func (dl *YouTubeDLSong) ID() string {
|
||||||
|
return dl.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filename returns the filename of the Song.
|
||||||
|
func (dl *YouTubeDLSong) Filename() string {
|
||||||
|
return dl.id + dl.format
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duration returns the duration of the Song.
|
||||||
|
func (dl *YouTubeDLSong) Duration() string {
|
||||||
|
return dl.duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thumbnail returns the thumbnail URL for the Song.
|
||||||
|
func (dl *YouTubeDLSong) Thumbnail() string {
|
||||||
|
return dl.thumbnail
|
||||||
|
}
|
||||||
|
|
||||||
|
// Playlist returns the playlist type for the Song (may be nil).
|
||||||
|
func (dl *YouTubeDLSong) Playlist() Playlist {
|
||||||
|
return dl.playlist
|
||||||
|
}
|
||||||
|
|
||||||
|
// DontSkip returns the DontSkip boolean value for the Song.
|
||||||
|
func (dl *YouTubeDLSong) DontSkip() bool {
|
||||||
|
return dl.dontSkip
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDontSkip sets the DontSkip boolean value for the Song.
|
||||||
|
func (dl *YouTubeDLSong) SetDontSkip(value bool) {
|
||||||
|
dl.dontSkip = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------
|
||||||
|
// YOUTUBE PLAYLIST
|
||||||
|
// ----------------
|
||||||
|
|
||||||
|
// AddSkip adds a skip to the playlist's skippers slice.
|
||||||
|
func (p *YouTubeDLPlaylist) 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 *YouTubeDLPlaylist) 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 *YouTubeDLPlaylist) 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 *YouTubeDLPlaylist) 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 YouTubeDLPlaylist.
|
||||||
|
func (p *YouTubeDLPlaylist) ID() string {
|
||||||
|
return p.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title returns the title of the YouTubeDLPlaylist.
|
||||||
|
func (p *YouTubeDLPlaylist) Title() string {
|
||||||
|
return p.title
|
||||||
|
}
|
Reference in a new issue