Fixed cache clearing earlier than expected
By default, the date on a file downloaded by youtube-dl is the date the video was originally uploaded to youtube. As a result, items will always be cleared from the cache because they appear to be years old - even though they were just downloaded. Adding the directive "--no-mtime" will make the date on files the date the file was downloaded. See this issue on youtube-dl: https://github.com/rg3/youtube-dl/issues/4667
This commit is contained in:
parent
b25f86d025
commit
7b96f4c3ce
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
README.md merge=ours
|
30
.travis.yml
Normal file
30
.travis.yml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
language: go
|
||||||
|
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- $HOME/opus
|
||||||
|
- $HOME/bin
|
||||||
|
- $HOME/gopath/bin
|
||||||
|
- $HOME/gopath/pkg
|
||||||
|
- $HOME/gopath/src/github.com/nitrous-io
|
||||||
|
- $HOME/gopath/src/github.com/MichaelOultram/mumbledj/.vendor
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- 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
|
||||||
|
|
||||||
|
env:
|
||||||
|
- PATH=$PATH:~/bin/
|
||||||
|
- PKG_CONFIG_PATH=$PKG_CONFIG_PATH:$HOME/opus/lib/pkgconfig
|
||||||
|
|
||||||
|
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
|
|
@ -1,6 +1,9 @@
|
||||||
MumbleDJ Changelog
|
MumbleDJ Changelog
|
||||||
==================
|
==================
|
||||||
|
|
||||||
|
### August 12, 2015 -- `v2.7.5`
|
||||||
|
* Fixed cache clearing earlier than expected (thanks [@CMahaff](https://github.com/CMahaff)).
|
||||||
|
|
||||||
### May 19, 2015 -- `v2.7.4`
|
### May 19, 2015 -- `v2.7.4`
|
||||||
* Fixed a panic that occurred when certain YouTube playlists were added to the queue.
|
* Fixed a panic that occurred when certain YouTube playlists were added to the queue.
|
||||||
|
|
||||||
|
|
8
Makefile
8
Makefile
|
@ -1,7 +1,7 @@
|
||||||
all: mumbledj
|
all: mumbledj
|
||||||
|
|
||||||
mumbledj: main.go commands.go parseconfig.go strings.go service.go service_youtube.go songqueue.go cache.go web.go
|
mumbledj: main.go commands.go parseconfig.go strings.go service.go youtube_dl.go service_youtube.go service_soundcloud.go songqueue.go cache.go
|
||||||
go get github.com/nitrous-io/goop
|
if [ ! -f $(GOPATH)/bin/goop ]; then go get github.com/nitrous-io/goop; fi;
|
||||||
rm -rf Goopfile.lock
|
rm -rf Goopfile.lock
|
||||||
goop install
|
goop install
|
||||||
goop go build
|
goop go build
|
||||||
|
@ -12,10 +12,8 @@ clean:
|
||||||
install:
|
install:
|
||||||
mkdir -p ~/.mumbledj/config
|
mkdir -p ~/.mumbledj/config
|
||||||
mkdir -p ~/.mumbledj/songs
|
mkdir -p ~/.mumbledj/songs
|
||||||
mkdir -p ~/.mumbledj/web
|
if [ -f ~/.mumbledj/config/mumbledj.gcfg ]; then mv ~/.mumbledj/config/mumbledj.gcfg ~/.mumbledj/config/mumbledj_backup.gcfg; fi;
|
||||||
if [ -a ~/.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
|
|
||||||
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;
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
MumbleDJ [![Build Status](https://drone.io/github.com/MichaelOultram/mumbledj/status.png)](https://drone.io/github.com/MichaelOultram/mumbledj/latest)
|
MumbleDJ [![Build Status](https://travis-ci.org/MichaelOultram/mumbledj.svg?branch=master)](https://travis-ci.org/MichaelOultram/mumbledj)
|
||||||
========
|
========
|
||||||
**A Mumble bot that plays music fetched from YouTube videos.**
|
**A Mumble bot that plays music fetched from YouTube videos.**
|
||||||
|
|
||||||
|
@ -51,7 +51,6 @@ Command | Description | Arguments | Admin | Example
|
||||||
**help** | Displays this list of commands in Mumble chat. | None | No | `!help`
|
**help** | Displays this list of commands in Mumble chat. | None | No | `!help`
|
||||||
**volume** | Either outputs the current volume or changes the current volume. If desired volume is not provided, the current volume will be displayed in chat. Otherwise, the volume for the bot will be changed to desired volume if it is within the allowed volume range. | None OR desired volume | No | `!volume 0.5`, `!volume`
|
**volume** | Either outputs the current volume or changes the current volume. If desired volume is not provided, the current volume will be displayed in chat. Otherwise, the volume for the bot will be changed to desired volume if it is within the allowed volume range. | None OR desired volume | No | `!volume 0.5`, `!volume`
|
||||||
**move** | Moves MumbleDJ into channel if it exists. | Channel | Yes | `!move Music`
|
**move** | Moves MumbleDJ into channel if it exists. | Channel | Yes | `!move Music`
|
||||||
**web** | Displays a unique url for the user to control mumbledj from a web browser | None | No | `!web`
|
|
||||||
**reload** | Reloads `mumbledj.gcfg` to retrieve updated configuration settings. | None | Yes | `!reload`
|
**reload** | Reloads `mumbledj.gcfg` to retrieve updated configuration settings. | None | Yes | `!reload`
|
||||||
**reset** | Stops all audio and resets the song queue. | None | Yes | `!reset`
|
**reset** | Stops all audio and resets the song queue. | None | Yes | `!reset`
|
||||||
**numsongs** | Outputs the number of songs in the queue in chat. Individual songs and songs within playlists are both counted. | None | No | `!numsongs`
|
**numsongs** | Outputs the number of songs in the queue in chat. Individual songs and songs within playlists are both counted. | None | No | `!numsongs`
|
||||||
|
|
27
circle.yml
Normal file
27
circle.yml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
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:
|
||||||
|
- bash install-dependencies.sh
|
||||||
|
|
||||||
|
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:
|
||||||
|
- mumbledj -server=$MUMBLE_IP -port=$MUMBLE_PORT -username=circleci -password=$MUMBLE_PASSWORD -verbose=true -test=true:
|
||||||
|
timeout: 180
|
20
commands.go
20
commands.go
|
@ -82,13 +82,6 @@ func parseCommand(user *gumble.User, username, command string) {
|
||||||
} else {
|
} else {
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
||||||
}
|
}
|
||||||
// Web command
|
|
||||||
case dj.conf.Aliases.WebAlias:
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminWeb) {
|
|
||||||
web.GetWebAddress(user)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
// Move command
|
// Move command
|
||||||
case dj.conf.Aliases.MoveAlias:
|
case dj.conf.Aliases.MoveAlias:
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminMove) {
|
if dj.HasPermission(username, dj.conf.Permissions.AdminMove) {
|
||||||
|
@ -159,6 +152,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 +171,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
10
config.gcfg
10
config.gcfg
|
@ -86,10 +86,6 @@ HelpAlias = "help"
|
||||||
# DEFAULT VALUE: "volume"
|
# DEFAULT VALUE: "volume"
|
||||||
VolumeAlias = "volume"
|
VolumeAlias = "volume"
|
||||||
|
|
||||||
# Alias used for web address command
|
|
||||||
# DEFAULT VALUE: "web"
|
|
||||||
WebAlias = "web"
|
|
||||||
|
|
||||||
# Alias used for move command
|
# Alias used for move command
|
||||||
# DEFAULT VALUE: "move"
|
# DEFAULT VALUE: "move"
|
||||||
MoveAlias = "move"
|
MoveAlias = "move"
|
||||||
|
@ -144,7 +140,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
|
||||||
|
@ -166,10 +162,6 @@ AdminHelp = false
|
||||||
# DEFAULT VALUE: false
|
# DEFAULT VALUE: false
|
||||||
AdminVolume = false
|
AdminVolume = false
|
||||||
|
|
||||||
# Make web an admin command?
|
|
||||||
# DEFAULT VALUE: false
|
|
||||||
AdminWeb = false
|
|
||||||
|
|
||||||
# Make move an admin command?
|
# Make move an admin command?
|
||||||
# DEFAULT VALUE: true
|
# DEFAULT VALUE: true
|
||||||
AdminMove = true
|
AdminMove = true
|
||||||
|
|
38
index.html
38
index.html
|
@ -1,38 +0,0 @@
|
||||||
<!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}}";
|
|
||||||
}
|
|
||||||
|
|
||||||
function addURL() {
|
|
||||||
var url = $("#textbox");
|
|
||||||
$.ajax(api("add") + "&value=" + url.attr("value"));
|
|
||||||
url.attr("value", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
function volume() {
|
|
||||||
var volume = $("#textbox");
|
|
||||||
$.ajax(api("volume") + "&value=" + volume.attr("value"));
|
|
||||||
volume.attr("value", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
</html>
|
|
37
install-dependencies.sh
Normal file
37
install-dependencies.sh
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# removing old ffmpeg
|
||||||
|
sudo rm -rf /usr/bin/ffmpeg
|
||||||
|
sudo rm -rf /usr/bin/X11/ffmpeg
|
||||||
|
sudo rm -rf /usr/share/man/man1/ffmpeg.1.gz
|
||||||
|
|
||||||
|
# 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* ~/bin
|
||||||
|
else
|
||||||
|
echo 'Using cached version of ffmpeg.';
|
||||||
|
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/latest/youtube-dl -o ~/bin/youtube-dl
|
||||||
|
chmod a+rx ~/bin/youtube-dl
|
||||||
|
else
|
||||||
|
echo 'Using cached version of youtube-dl.';
|
||||||
|
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
|
||||||
|
else
|
||||||
|
echo 'Using cached version of opus.';
|
||||||
|
fi
|
18
main.go
18
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()
|
||||||
|
@ -172,8 +174,6 @@ var dj = mumbledj{
|
||||||
cache: NewSongCache(),
|
cache: NewSongCache(),
|
||||||
}
|
}
|
||||||
|
|
||||||
var web *WebServer
|
|
||||||
|
|
||||||
// main primarily performs startup tasks. Grabs and parses commandline
|
// main primarily performs startup tasks. Grabs and parses commandline
|
||||||
// args, sets up the gumble client and its listeners, and then connects to the server.
|
// args, sets up the gumble client and its listeners, and then connects to the server.
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -191,7 +191,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 +202,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,12 +245,9 @@ func main() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
web = NewWebServer(9563)
|
if testcode {
|
||||||
web.makeWeb()
|
Verbose("Testing is enabled")
|
||||||
|
Test(password, address, port, strings.Split(accesstokens, " "))
|
||||||
if isNil(web) {
|
|
||||||
Verbose("WEB IS NIL")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<-dj.keepAlive
|
<-dj.keepAlive
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,6 @@ type DjConfig struct {
|
||||||
NumCachedAlias string
|
NumCachedAlias string
|
||||||
CacheSizeAlias string
|
CacheSizeAlias string
|
||||||
KillAlias string
|
KillAlias string
|
||||||
WebAlias string
|
|
||||||
}
|
}
|
||||||
Permissions struct {
|
Permissions struct {
|
||||||
AdminsEnabled bool
|
AdminsEnabled bool
|
||||||
|
@ -71,7 +70,6 @@ type DjConfig struct {
|
||||||
AdminNumCached bool
|
AdminNumCached bool
|
||||||
AdminCacheSize bool
|
AdminCacheSize bool
|
||||||
AdminKill bool
|
AdminKill bool
|
||||||
AdminWeb bool
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
@ -96,7 +73,12 @@ func (yt YouTube) NewRequest(user *gumble.User, url string) (string, error) {
|
||||||
startOffset = matches[0][2]
|
startOffset = matches[0][2]
|
||||||
}
|
}
|
||||||
song, err := yt.NewSong(user.Name, shortURL, startOffset, nil)
|
song, err := yt.NewSong(user.Name, shortURL, startOffset, nil)
|
||||||
|
if !isNil(song) {
|
||||||
return song.Title(), err
|
return song.Title(), err
|
||||||
|
} else {
|
||||||
|
Verbose("youtube.NewRequest: " + err.Error())
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -105,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,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,
|
||||||
|
@ -194,67 +177,20 @@ func (yt YouTube) NewSong(user, id, offset string, playlist *YouTubePlaylist) (*
|
||||||
dj.queue.AddSong(song)
|
dj.queue.AddSong(song)
|
||||||
Verbose(song.Submitter() + " added track " + song.Title())
|
Verbose(song.Submitter() + " added track " + song.Title())
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
return song, nil
|
return song, nil
|
||||||
}
|
=======
|
||||||
return nil, errors.New(VIDEO_TOO_LONG_MSG)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPlaylist gathers the metadata for a YouTube playlist and returns it.
|
|
||||||
func (yt YouTube) NewPlaylist(user, id string) (*YouTubePlaylist, error) {
|
|
||||||
var apiResponse *jsonq.JsonQuery
|
|
||||||
var err error
|
|
||||||
// Retrieve title of playlist
|
|
||||||
url := fmt.Sprintf("https://www.googleapis.com/youtube/v3/playlists?part=snippet&id=%s&key=%s",
|
|
||||||
id, os.Getenv("YOUTUBE_API_KEY"))
|
|
||||||
if apiResponse, err = yt.PerformGetRequest(url); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
title, _ := apiResponse.String("items", "0", "snippet", "title")
|
|
||||||
|
|
||||||
playlist := &YouTubePlaylist{
|
|
||||||
id: id,
|
|
||||||
title: title,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve items in playlist
|
|
||||||
url = fmt.Sprintf("https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=50&playlistId=%s&key=%s",
|
|
||||||
id, os.Getenv("YOUTUBE_API_KEY"))
|
|
||||||
if apiResponse, err = yt.PerformGetRequest(url); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
numVideos, _ := apiResponse.Int("pageInfo", "totalResults")
|
|
||||||
if numVideos > 50 {
|
|
||||||
numVideos = 50
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < numVideos; i++ {
|
|
||||||
index := strconv.Itoa(i)
|
|
||||||
videoID, _ := apiResponse.String("items", index, "snippet", "resourceId", "videoId")
|
|
||||||
yt.NewSong(user, videoID, "", playlist)
|
|
||||||
}
|
|
||||||
return playlist, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------
|
|
||||||
// YOUTUBE SONG
|
|
||||||
// ------------
|
|
||||||
|
|
||||||
// Download downloads the song via youtube-dl if it does not already exist on disk.
|
// 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.
|
// All downloaded songs are stored in ~/.mumbledj/songs and should be automatically cleaned.
|
||||||
func (s *YouTubeSong) Download() error {
|
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) {
|
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", "--no-mtime", "--output", fmt.Sprintf(`~/.mumbledj/songs/%s`, s.Filename()), "--format", "m4a", "--", s.ID())
|
||||||
cmd := exec.Command("youtube-dl", "--output", fmt.Sprintf(`~/.mumbledj/songs/%s`, s.Filename()), "--format", "m4a", "--", s.ID())
|
|
||||||
if err := cmd.Run(); err == nil {
|
if err := cmd.Run(); err == nil {
|
||||||
if dj.conf.Cache.Enabled {
|
if dj.conf.Cache.Enabled {
|
||||||
dj.cache.CheckMaximumDirectorySize()
|
dj.cache.CheckMaximumDirectorySize()
|
||||||
}
|
}
|
||||||
Verbose(s.Title() + " downloaded")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
Verbose(s.Title() + " failed to download")
|
|
||||||
return errors.New("Song download failed.")
|
return errors.New("Song download failed.")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -271,7 +207,7 @@ func (s *YouTubeSong) Play() {
|
||||||
if err := dj.audioStream.Play(); err != nil {
|
if err := dj.audioStream.Play(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
} else {
|
} else {
|
||||||
if isNil(s.Playlist()) {
|
if s.Playlist() == nil {
|
||||||
message := `
|
message := `
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -307,8 +243,6 @@ func (s *YouTubeSong) Play() {
|
||||||
dj.client.Self.Channel.Send(fmt.Sprintf(message, s.Thumbnail(), s.ID(),
|
dj.client.Self.Channel.Send(fmt.Sprintf(message, s.Thumbnail(), s.ID(),
|
||||||
s.Title(), s.Duration(), s.Submitter(), s.Playlist().Title()), false)
|
s.Title(), s.Duration(), s.Submitter(), s.Playlist().Title()), false)
|
||||||
}
|
}
|
||||||
Verbose("Now playing " + s.Title())
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
dj.audioStream.Wait()
|
dj.audioStream.Wait()
|
||||||
dj.queue.OnSongFinished()
|
dj.queue.OnSongFinished()
|
||||||
|
@ -322,10 +256,8 @@ func (s *YouTubeSong) Delete() error {
|
||||||
filePath := fmt.Sprintf("%s/.mumbledj/songs/%s.m4a", dj.homeDir, s.ID())
|
filePath := fmt.Sprintf("%s/.mumbledj/songs/%s.m4a", dj.homeDir, s.ID())
|
||||||
if _, err := os.Stat(filePath); err == nil {
|
if _, err := os.Stat(filePath); err == nil {
|
||||||
if err := os.Remove(filePath); err == nil {
|
if err := os.Remove(filePath); err == nil {
|
||||||
Verbose("Deleted " + s.Title())
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
Verbose("Failed to delete " + s.Title())
|
|
||||||
return errors.New("Error occurred while deleting audio file.")
|
return errors.New("Error occurred while deleting audio file.")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -363,113 +295,48 @@ func (s *YouTubeSong) RemoveSkip(username string) error {
|
||||||
func (s *YouTubeSong) SkipReached(channelUsers int) bool {
|
func (s *YouTubeSong) SkipReached(channelUsers int) bool {
|
||||||
if float32(len(s.skippers))/float32(channelUsers) >= dj.conf.General.SkipRatio {
|
if float32(len(s.skippers))/float32(channelUsers) >= dj.conf.General.SkipRatio {
|
||||||
return true
|
return true
|
||||||
|
>>>>>>> 2df3613... Fixed cache clearing earlier than expected
|
||||||
}
|
}
|
||||||
return false
|
return nil, errors.New(VIDEO_TOO_LONG_MSG)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submitter returns the name of the submitter of the YouTubeSong.
|
// NewPlaylist gathers the metadata for a YouTube playlist and returns it.
|
||||||
func (s *YouTubeSong) Submitter() string {
|
func (yt YouTube) NewPlaylist(user *gumble.User, id string) (Playlist, error) {
|
||||||
return s.submitter
|
var apiResponse *jsonq.JsonQuery
|
||||||
|
var err error
|
||||||
|
// Retrieve title of playlist
|
||||||
|
url := fmt.Sprintf("https://www.googleapis.com/youtube/v3/playlists?part=snippet&id=%s&key=%s", id, os.Getenv("YOUTUBE_API_KEY"))
|
||||||
|
if apiResponse, err = PerformGetRequest(url); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
title, _ := apiResponse.String("items", "0", "snippet", "title")
|
||||||
|
|
||||||
|
playlist := &YouTubeDLPlaylist{
|
||||||
|
id: id,
|
||||||
|
title: title,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Title returns the title of the YouTubeSong.
|
// Retrieve items in playlist
|
||||||
func (s *YouTubeSong) Title() string {
|
url = fmt.Sprintf("https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=50&playlistId=%s&key=%s",
|
||||||
return s.title
|
id, os.Getenv("YOUTUBE_API_KEY"))
|
||||||
|
if apiResponse, err = PerformGetRequest(url); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
numVideos, _ := apiResponse.Int("pageInfo", "totalResults")
|
||||||
|
if numVideos > 50 {
|
||||||
|
numVideos = 50
|
||||||
}
|
}
|
||||||
|
|
||||||
// ID returns the id of the YouTubeSong.
|
for i := 0; i < numVideos; i++ {
|
||||||
func (s *YouTubeSong) ID() string {
|
index := strconv.Itoa(i)
|
||||||
return s.id
|
videoID, _ := apiResponse.String("items", index, "snippet", "resourceId", "videoId")
|
||||||
|
yt.NewSong(user, videoID, "", playlist)
|
||||||
}
|
}
|
||||||
|
return playlist, nil
|
||||||
// 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 {
|
||||||
|
@ -482,7 +349,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))
|
||||||
|
|
|
@ -170,8 +170,3 @@ const CURRENT_SONG_HTML = `
|
||||||
const CURRENT_SONG_PLAYLIST_HTML = `
|
const CURRENT_SONG_PLAYLIST_HTML = `
|
||||||
The song currently playing is "%s", added <b>%s</b> from the playlist "%s".
|
The song currently playing is "%s", added <b>%s</b> from the playlist "%s".
|
||||||
`
|
`
|
||||||
|
|
||||||
// URL of the server for connecting via a web address
|
|
||||||
const WEB_ADDRESS = `
|
|
||||||
Control mumbledj from a web browser: <a href="http://%s:9563/%s">http://%s:9563/%s</a>
|
|
||||||
`
|
|
||||||
|
|
85
test.go
Normal file
85
test.go
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"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 {
|
||||||
|
config := gumble.Config{
|
||||||
|
Username: uname,
|
||||||
|
Password: t.password,
|
||||||
|
Address: t.ip + ":" + t.port,
|
||||||
|
Tokens: t.accesstokens,
|
||||||
|
}
|
||||||
|
config.TLSConfig.InsecureSkipVerify = true
|
||||||
|
client := gumble.NewClient(&config)
|
||||||
|
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("dummy")
|
||||||
|
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 * 10)
|
||||||
|
skip(dummyUser, false, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(0)
|
||||||
|
dummyClient.Disconnect()
|
||||||
|
}
|
146
web.go
146
web.go
|
@ -1,146 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"html"
|
|
||||||
"html/template"
|
|
||||||
"io/ioutil"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/layeh/gumble/gumble"
|
|
||||||
)
|
|
||||||
|
|
||||||
type WebServer struct {
|
|
||||||
port int
|
|
||||||
client_token map[*gumble.User]string
|
|
||||||
token_client map[string]*gumble.User
|
|
||||||
}
|
|
||||||
|
|
||||||
type Page struct {
|
|
||||||
Site string
|
|
||||||
Token string
|
|
||||||
User string
|
|
||||||
}
|
|
||||||
|
|
||||||
var external_ip = ""
|
|
||||||
|
|
||||||
func NewWebServer(port int) *WebServer {
|
|
||||||
rand.Seed(time.Now().UnixNano())
|
|
||||||
return &WebServer{
|
|
||||||
port: port,
|
|
||||||
client_token: make(map[*gumble.User]string),
|
|
||||||
token_client: make(map[string]*gumble.User),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (web *WebServer) makeWeb() {
|
|
||||||
http.HandleFunc("/", web.homepage)
|
|
||||||
http.HandleFunc("/add", web.add)
|
|
||||||
http.HandleFunc("/volume", web.volume)
|
|
||||||
http.HandleFunc("/skip", web.skip)
|
|
||||||
http.ListenAndServe(":"+strconv.Itoa(web.port), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (web *WebServer) homepage(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var uname = web.token_client[r.URL.Path[1:]]
|
|
||||||
if uname == nil {
|
|
||||||
fmt.Fprintf(w, "Invalid Token")
|
|
||||||
} else {
|
|
||||||
cwd, _ := os.Getwd()
|
|
||||||
t, err := template.ParseFiles(filepath.Join(cwd, "./.mumbledj/web/index.html"))
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = t.Execute(w, Page{getIP() + ":" + strconv.Itoa(web.port), r.URL.Path[1:], uname.Name})
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (web *WebServer) add(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var uname = web.token_client[r.FormValue("token")]
|
|
||||||
if uname == nil {
|
|
||||||
fmt.Fprintf(w, "Invalid Token")
|
|
||||||
} else {
|
|
||||||
add(uname, html.UnescapeString(r.FormValue("value")))
|
|
||||||
fmt.Fprintf(w, "Success")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (web *WebServer) volume(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var uname = web.token_client[r.FormValue("token")]
|
|
||||||
if uname == nil {
|
|
||||||
fmt.Fprintf(w, "Invalid Token")
|
|
||||||
} else {
|
|
||||||
var vol = html.UnescapeString(r.FormValue("value"))
|
|
||||||
volume(uname, vol)
|
|
||||||
fmt.Fprintf(w, "Success")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (web *WebServer) skip(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var uname = web.token_client[r.FormValue("token")]
|
|
||||||
if uname == nil {
|
|
||||||
fmt.Fprintf(w, "Invalid Token")
|
|
||||||
} else {
|
|
||||||
value := html.UnescapeString(r.FormValue("value"))
|
|
||||||
playlist, err := strconv.ParseBool(value)
|
|
||||||
if err == nil {
|
|
||||||
skip(uname, false, playlist)
|
|
||||||
fmt.Fprintf(w, "Success")
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(w, "Invalid Value")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (website *WebServer) GetWebAddress(user *gumble.User) {
|
|
||||||
Verbose("Port number: " + strconv.Itoa(web.port))
|
|
||||||
if web.client_token[user] != "" {
|
|
||||||
web.token_client[web.client_token[user]] = nil
|
|
||||||
}
|
|
||||||
// dealing with collisions
|
|
||||||
var firstLoop = true
|
|
||||||
for firstLoop || web.token_client[web.client_token[user]] != nil {
|
|
||||||
web.client_token[user] = randSeq(10)
|
|
||||||
firstLoop = false
|
|
||||||
}
|
|
||||||
web.token_client[web.client_token[user]] = user
|
|
||||||
dj.SendPrivateMessage(user, fmt.Sprintf(WEB_ADDRESS, getIP(), web.client_token[user], getIP(), web.client_token[user]))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets the external ip address for the server
|
|
||||||
func getIP() string {
|
|
||||||
if external_ip != "" {
|
|
||||||
return external_ip
|
|
||||||
} else {
|
|
||||||
if response, err := http.Get("http://myexternalip.com/raw"); err == nil {
|
|
||||||
defer response.Body.Close()
|
|
||||||
if response.StatusCode == 200 {
|
|
||||||
if body, err := ioutil.ReadAll(response.Body); err == nil {
|
|
||||||
external_ip = strings.TrimSpace(string(body))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return external_ip
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generates a pseudorandom string of characters
|
|
||||||
func randSeq(n int) string {
|
|
||||||
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
|
||||||
b := make([]rune, n)
|
|
||||||
for i := range b {
|
|
||||||
b[i] = letters[rand.Intn(len(letters))]
|
|
||||||
}
|
|
||||||
return string(b)
|
|
||||||
}
|
|
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