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
|
||||
|
||||
before_script:
|
||||
<<<<<<< HEAD
|
||||
- export PATH=$PATH:~/bin/
|
||||
- export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:$HOME/opus/lib/pkgconfig
|
||||
- bash install-dependencies.sh
|
||||
|
||||
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/songs
|
||||
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 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;
|
||||
|
|
|
@ -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.**
|
||||
|
||||
|
|
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 {
|
||||
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:
|
||||
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)
|
||||
return errors.New("NO_ARGUMENT")
|
||||
} 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"
|
||||
# line of code. Each line has one username, and an unlimited amount of usernames may
|
||||
# be entered in this matter.
|
||||
Admins = "Matt"
|
||||
Admins = "BottleOToast"
|
||||
|
||||
# Make add an admin command?
|
||||
# DEFAULT VALUE: false
|
||||
|
|
75
index.html
75
index.html
|
@ -1,38 +1,49 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="ISO-8859-1">
|
||||
<title>{{.User}} - mumbledj</title>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
function api(type) {
|
||||
return "http://{{.Site}}/" + type + "?token={{.Token}}";
|
||||
}
|
||||
<head>
|
||||
<meta charset="ISO-8859-1">
|
||||
<title>{{.User}} - mumbledj</title>
|
||||
<script
|
||||
src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
function onLoad() {
|
||||
window.setInterval(function() {
|
||||
// Get the song queue
|
||||
}, 6000);
|
||||
}
|
||||
|
||||
function addURL() {
|
||||
var url = $("#textbox");
|
||||
$.ajax(api("add") + "&value=" + url.attr("value"));
|
||||
url.attr("value", "");
|
||||
}
|
||||
function setAPI(type, val) {
|
||||
$.ajax({
|
||||
url : "http://{{.Site}}/api/" + type + "?token={{.Token}}"
|
||||
+ "&value=" + val,
|
||||
complete : apiComplete,
|
||||
cache : false
|
||||
});
|
||||
}
|
||||
|
||||
function volume() {
|
||||
var volume = $("#textbox");
|
||||
$.ajax(api("volume") + "&value=" + volume.attr("value"));
|
||||
volume.attr("value", "");
|
||||
}
|
||||
function apiComplete(jqXHR, textStatus) {
|
||||
alert(textStatus);
|
||||
}
|
||||
|
||||
function skip(val) {
|
||||
$.ajax(api("skip") + "&value=" + val);
|
||||
}
|
||||
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Add Song Form</h1>
|
||||
<input id="textbox" type="text"/>
|
||||
<input id="add" type="button" value="Add Song" onclick="addURL()"/>
|
||||
<input id="volume" type="button" value="Set Volume" onclick="volume()"/>
|
||||
<input id="skipSong" type="button" value="Skip Current Song" onclick="skip('false')"/>
|
||||
<input id="skipPlaylist" type="button" value="Skip Current Playlist" onclick="skip('true')"/>
|
||||
</body>
|
||||
function txtBox(type) {
|
||||
var txt = $("#textbox");
|
||||
api(type, txt.attr("value"));
|
||||
txt.attr("value", "");
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body onload="onLoad();" bgcolor="#FFFFFF">
|
||||
<h1>Add Song Form</h1>
|
||||
<input id="textbox" type="text" />
|
||||
<input id="add" type="button" value="Add Song"
|
||||
onclick="setAPI('add', $('#textbox').attr('value'))" />
|
||||
<input id="volume" type="button" value="Set Volume"
|
||||
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>
|
|
@ -3,16 +3,18 @@ set -e
|
|||
|
||||
# check to see if ffmpeg is installed
|
||||
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
|
||||
tar -xvf /tmp/ffmpeg.tar.xz --strip 1 --no-anchored ffmpeg ffprobe
|
||||
chmod a+rx ffmpeg ffprobe
|
||||
mv ff* $HOME/bin
|
||||
mv ff* ~/bin
|
||||
else
|
||||
echo 'Using cached version of ffmpeg.';
|
||||
fi
|
||||
|
||||
# check to see if opus is installed
|
||||
if [ ! -d "$HOME/opus/lib" ]; then
|
||||
echo 'Installing opus'
|
||||
wget http://downloads.xiph.org/releases/opus/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
|
||||
|
@ -22,6 +24,7 @@ fi
|
|||
|
||||
# check to see if youtube-dl is installed
|
||||
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
|
||||
chmod a+rx ~/bin/youtube-dl
|
||||
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) {
|
||||
if dj.verbose {
|
||||
fmt.Printf(msg + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Checks to see if an object is nil
|
||||
func isNil(a interface{}) bool {
|
||||
defer func() { recover() }()
|
||||
return a == nil || reflect.ValueOf(a).IsNil()
|
||||
|
@ -191,7 +193,7 @@ func main() {
|
|||
}
|
||||
|
||||
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(&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(&accesstokens, "accesstokens", "", "list of access tokens for channel auth")
|
||||
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()
|
||||
|
||||
dj.config = gumble.Config{
|
||||
|
@ -244,6 +247,11 @@ func main() {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
if testcode {
|
||||
Verbose("Testing is enabled")
|
||||
Test(password, address, port, strings.Split(accesstokens, " "))
|
||||
}
|
||||
|
||||
web = NewWebServer(9563)
|
||||
web.makeWeb()
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ type Playlist interface {
|
|||
Title() string
|
||||
}
|
||||
|
||||
var services = []Service{YouTube{}}
|
||||
var services = []Service{YouTube{}, SoundCloud{}}
|
||||
|
||||
func findServiceAndAdd(user *gumble.User, url string) error {
|
||||
var urlService Service
|
||||
|
@ -65,6 +65,7 @@ func findServiceAndAdd(user *gumble.User, url string) error {
|
|||
}
|
||||
|
||||
if urlService == nil {
|
||||
Verbose("Invalid_URL")
|
||||
return errors.New("INVALID_URL")
|
||||
} else {
|
||||
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"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jmoiron/jsonq"
|
||||
"github.com/layeh/gumble/gumble"
|
||||
"github.com/layeh/gumble/gumble_ffmpeg"
|
||||
)
|
||||
|
||||
// Regular expressions for youtube urls
|
||||
|
@ -42,26 +39,6 @@ var youtubeVideoPatterns = []string{
|
|||
// YouTube implements the Service interface
|
||||
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
|
||||
// ---------------
|
||||
|
@ -83,7 +60,7 @@ func (yt YouTube) NewRequest(user *gumble.User, url string) (string, error) {
|
|||
if re.MatchString(url) {
|
||||
if dj.HasPermission(user.Name, dj.conf.Permissions.AdminAddPlaylists) {
|
||||
shortURL = re.FindStringSubmatch(url)[1]
|
||||
playlist, err := yt.NewPlaylist(user.Name, shortURL)
|
||||
playlist, err := yt.NewPlaylist(user, shortURL)
|
||||
return playlist.Title(), err
|
||||
} else {
|
||||
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 {
|
||||
startOffset = matches[0][2]
|
||||
}
|
||||
song, err := yt.NewSong(user.Name, shortURL, startOffset, nil)
|
||||
song, err := yt.NewSong(user, shortURL, startOffset, nil)
|
||||
if !isNil(song) {
|
||||
return song.Title(), err
|
||||
return song.Title(), nil
|
||||
} else {
|
||||
Verbose("youtube.NewRequest: " + err.Error())
|
||||
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
|
||||
// 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 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 = yt.PerformGetRequest(url); err != nil {
|
||||
if apiResponse, err = PerformGetRequest(url); err != nil {
|
||||
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 {
|
||||
song := &YouTubeSong{
|
||||
song := &YouTubeDLSong{
|
||||
submitter: user,
|
||||
title: title,
|
||||
id: id,
|
||||
url: "https://youtu.be/" + id,
|
||||
offset: int((offsetDays * 86400) + (offsetHours * 3600) + (offsetMinutes * 60) + offsetSeconds),
|
||||
filename: id + ".m4a",
|
||||
duration: durationString,
|
||||
thumbnail: thumbnail,
|
||||
format: "m4a",
|
||||
skippers: make([]string, 0),
|
||||
playlist: playlist,
|
||||
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.
|
||||
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 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 = yt.PerformGetRequest(url); err != nil {
|
||||
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{
|
||||
playlist := &YouTubeDLPlaylist{
|
||||
id: id,
|
||||
title: title,
|
||||
}
|
||||
|
@ -223,7 +201,7 @@ func (yt YouTube) NewPlaylist(user, id string) (*YouTubePlaylist, error) {
|
|||
// 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 = yt.PerformGetRequest(url); err != nil {
|
||||
if apiResponse, err = PerformGetRequest(url); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
numVideos, _ := apiResponse.Int("pageInfo", "totalResults")
|
||||
|
@ -239,241 +217,8 @@ func (yt YouTube) NewPlaylist(user, id string) (*YouTubePlaylist, error) {
|
|||
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.
|
||||
func (yt YouTube) PerformGetRequest(url string) (*jsonq.JsonQuery, error) {
|
||||
func PerformGetRequest(url string) (*jsonq.JsonQuery, error) {
|
||||
jsonString := ""
|
||||
|
||||
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 {
|
||||
return nil, errors.New("Invalid API key supplied.")
|
||||
}
|
||||
return nil, errors.New("Invalid YouTube ID supplied.")
|
||||
return nil, errors.New("Invalid ID supplied.")
|
||||
}
|
||||
} else {
|
||||
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).
|
||||
func (q *SongQueue) OnSongFinished() {
|
||||
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
|
||||
|
||||
import (
|
||||
//"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
|
@ -8,7 +9,6 @@ import (
|
|||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -28,6 +28,21 @@ type Page struct {
|
|||
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 = ""
|
||||
|
||||
func NewWebServer(port int) *WebServer {
|
||||
|
@ -41,9 +56,10 @@ func NewWebServer(port int) *WebServer {
|
|||
|
||||
func (web *WebServer) makeWeb() {
|
||||
http.HandleFunc("/", web.homepage)
|
||||
http.HandleFunc("/add", web.add)
|
||||
http.HandleFunc("/volume", web.volume)
|
||||
http.HandleFunc("/skip", web.skip)
|
||||
http.HandleFunc("/api/add", web.add)
|
||||
http.HandleFunc("/api/volume", web.volume)
|
||||
http.HandleFunc("/api/skip", web.skip)
|
||||
//http.HandleFunc("/api/status", web.status)
|
||||
http.ListenAndServe(":"+strconv.Itoa(web.port), nil)
|
||||
}
|
||||
|
||||
|
@ -52,8 +68,14 @@ func (web *WebServer) homepage(w http.ResponseWriter, r *http.Request) {
|
|||
if uname == nil {
|
||||
fmt.Fprintf(w, "Invalid Token")
|
||||
} else {
|
||||
cwd, _ := os.Getwd()
|
||||
t, err := template.ParseFiles(filepath.Join(cwd, "./.mumbledj/web/index.html"))
|
||||
var webpage = uname.Name
|
||||
|
||||
// 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 {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
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) {
|
||||
Verbose("Port number: " + strconv.Itoa(web.port))
|
||||
if web.client_token[user] != "" {
|
||||
|
@ -110,7 +161,7 @@ func (website *WebServer) GetWebAddress(user *gumble.User) {
|
|||
}
|
||||
// dealing with collisions
|
||||
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)
|
||||
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