Bump to version 3.0.0
This commit is contained in:
parent
dcd2e1315f
commit
b32d064480
20
.github/ISSUE_TEMPLATE.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
#### Before submitting this issue, please acknowledge that you have done the following:
|
||||||
|
- [ ] I've at least skimmed through the [README](https://github.com/matthieugrieger/mumbledj/blob/master/README.md)
|
||||||
|
- [ ] I have checked that I am running the latest version of MumbleDJ (use `mumbledj --version` when starting the bot or use the MumbleDJ version command in Mumble)
|
||||||
|
- [ ] I have [searched through the existing issues](https://github.com/matthieugrieger/mumbledj/issues) to see if my issue has been answered already
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### What type of issue is this?
|
||||||
|
- [ ] Bug report (encountered problems with MumbleDJ)
|
||||||
|
- [ ] Feature request (request for a new functionality)
|
||||||
|
- [ ] Question
|
||||||
|
- [ ] Other:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Log output of bot with `--debug` flag (likely only for bug reports):
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Description of your issue:
|
18
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
18
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
#### Before submitting this pull request, please acknowledge that you have done the following:
|
||||||
|
- [ ] I have tested my changes to make sure that they work
|
||||||
|
- [ ] I have at least attempted to write relevant unit tests to verify that my changes work
|
||||||
|
- [ ] I have read through the [contribution guidelines](https://github.com/matthieugrieger/mumbledj/blob/master/.github/CONTRIBUTING.md)
|
||||||
|
- [ ] I have written any necessary documentation (for example, adding information about a feature to the README)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### What type of pull request is this?
|
||||||
|
- [ ] Bug fix
|
||||||
|
- [ ] Typo fix
|
||||||
|
- [ ] New feature implementation
|
||||||
|
- [ ] New service implementation
|
||||||
|
- [ ] Other
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Description of your pull request:
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1,4 +1 @@
|
||||||
mumbledj
|
mumbledj*
|
||||||
Goopfile.lock
|
|
||||||
.vendor
|
|
||||||
.project
|
|
||||||
|
|
27
.travis.yml
Normal file
27
.travis.yml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
language: go
|
||||||
|
|
||||||
|
sudo: false
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.5
|
||||||
|
- 1.6
|
||||||
|
- tip
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- go get github.com/Masterminds/glide
|
||||||
|
- glide install
|
||||||
|
- go get github.com/mattn/goveralls
|
||||||
|
- go get golang.org/x/tools/cmd/cover
|
||||||
|
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- refactor
|
||||||
|
|
||||||
|
script:
|
||||||
|
- make test
|
||||||
|
- "$HOME/gopath/bin/goveralls -service=travis-ci"
|
||||||
|
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
- GO15VENDOREXPERIMENT="1"
|
||||||
|
- secure: Ma6Htm+/fuCX9Wp5JEjA8qpRfsOSkveuSdegF9qbfdTjRL1GsaW7lU3IvFleUR9gclWaIhQYnAL0ZfELjcGGCLCV8b6hhK3TQ285ja3ue7/Ny5oaqwsej+MkdGQ1ZOGwIJi0KTwXHj0mgR6jrtjTSaMJ9ooQZ2DLizZ6FsQd5aXByQxz0cLLzg54ii1lXaV6XN+HCNXuWJfO/bH8KXI5aBU8QfoOkSCg7FUbkgckGnvCUpJkbs/cjpBOlnIRid7QiHHydwbERYTfJd7VOpY7wv+4rR2V4PB8OhpIWTA1Q/Z/6PEORq3r2P2IyMRpoJdCiEI1mKq9++OVP5GbxXWfND7P6n+o0wg5i/gp40witEMSarFtdoYOFaHBLHX+/Gf1TtqAR35pYW9rXCCzzul1r6DEDXUYZ4Z+N750Pwp+vSYsaDqGpl28btEwgK7jNF5cI3zj7ewwFodsOS+iXHYf9D2Lb8lCnmZjIrcP9RPOBm7xpiMI3eXD+gBcP/jk8+M92aroVmerK+QLLK5sGqdmLf9DzqJ8PqyqvGJMyk2WB40fNLNjHt2Zv/UU28Y2oXy9940Q77OkJZ5HVIixzYXL1hCF1+Mxb2hwZqFtrYDUj/Gficy+DKfpYAuTgD2gCSaCSq5tQpC2goFSg7oIY/R14Bfe6fyDuaBEtodkFZJ3B1E=
|
31
CHANGELOG.md
31
CHANGELOG.md
|
@ -1,6 +1,35 @@
|
||||||
MumbleDJ Changelog
|
MumbleDJ Changelog
|
||||||
==================
|
==================
|
||||||
|
|
||||||
|
### June 20, 2016 -- `v3.0.0`
|
||||||
|
* Significantly simplified installation process, now installable via `go install`.
|
||||||
|
* Commands may now have multiple aliases, configurable via config file.
|
||||||
|
* Command aliases are checked for duplicates and closes the bot if duplicates are found.
|
||||||
|
* Added new commands: `!resume`, `!pause`. These commands allow you to resume and pause audio streams respectively.
|
||||||
|
* `!add` and `!addnext` can now take any number of space-separated URLs as arguments.
|
||||||
|
* Commands are now executed asynchronously for better performance and responsiveness.
|
||||||
|
* Restructured project into subpackages: `bot`, `commands`, `interfaces`, `services`.
|
||||||
|
* Config file is now in `.yaml` format and is written to `$HOME/.config/mumbledj/config.yaml`.
|
||||||
|
* Altered config file layout to make it easier to read.
|
||||||
|
* When an updated config file is available it is written to `$HOME/.config/mumbledj/config.yaml.new`.
|
||||||
|
* Alternate config file locations can be supplied via `--config` commandline flag.
|
||||||
|
* Added logging for easier monitoring and issue debugging.
|
||||||
|
* Added `--debug` flag for more verbose logging when debugging an issue.
|
||||||
|
* Access tokens in config file and `--accesstokens` commandline flag are now comma-separated instead of space-separated.
|
||||||
|
* Mixcloud now requires the installation of `aria2` to work properly due to download throttling.
|
||||||
|
* Startup checks are performed before the bot connects to the server to determine if any required software is missing or misconfigured.
|
||||||
|
* API startup checks are performed before the bot connects to the server to determine if any services have missing/invalid API keys.
|
||||||
|
* Dependencies are now vendored via `/vendor` folder for more reproducible builds.
|
||||||
|
* `glide` has replaced `goop` as the dependency management tool.
|
||||||
|
* Added `CONTRIBUTING.md` and templates for GitHub Issues and Pull Requests.
|
||||||
|
* Revamped `Makefile` and made it less complicated.
|
||||||
|
* Implemented continuous integration support with [Travis CI](https://travis-ci.org).
|
||||||
|
* Builds for `linux/arm64` and `linux/386` are now provided as downloads for each release.
|
||||||
|
* Implemented many unit tests to test functionality of bot subsystems.
|
||||||
|
* Much more not listed here!
|
||||||
|
|
||||||
|
I hope you guys enjoy this update, it has been in the works for a few months. :)
|
||||||
|
|
||||||
### June 17, 2016 -- `v2.10.0`
|
### June 17, 2016 -- `v2.10.0`
|
||||||
* Added `!joinme` command (thanks [@azlux](https://github.com/azlux)).
|
* Added `!joinme` command (thanks [@azlux](https://github.com/azlux)).
|
||||||
|
|
||||||
|
@ -322,4 +351,4 @@ to the root of the server instead.
|
||||||
* mumble-music-bot repository created.
|
* mumble-music-bot repository created.
|
||||||
* Added config.py with some basic configuration options.
|
* Added config.py with some basic configuration options.
|
||||||
* Put placeholder methods within the MusicBot object.
|
* Put placeholder methods within the MusicBot object.
|
||||||
* Add run_bot.py.
|
* Add run_bot.py.
|
260
CONTRIBUTING.md
Normal file
260
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,260 @@
|
||||||
|
Contributing to MumbleDJ
|
||||||
|
========================
|
||||||
|
|
||||||
|
Contributions are always welcome to MumbleDJ. This document will give you some tips and guidelines to follow while implementing your contribution.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
* [Implementing a new command](#implementing-a-new-command)
|
||||||
|
* [Create files for your new command](#create-files-for-your-new-command)
|
||||||
|
* [Copy templates into your new files](#copy-templates-into-your-new-files)
|
||||||
|
* [Command implementation template (`command.go`)](#command-implementation-template-commandgo)
|
||||||
|
* [Command test suite template (`command_test.go`)](#command-test-suite-template-command_testgo)
|
||||||
|
* [Implement your new command](#implement-your-new-command)
|
||||||
|
* [Add command to `commands/pkg_init.go`](#add-command-to-commandspkg_initgo)
|
||||||
|
* [Add necessary configuration values to `config.yaml` and `config.go`](#add-necessary-configuration-values-to-configyaml-and-configgo)
|
||||||
|
* [Regenerate `bindata.go`](#regenerate-bindatago)
|
||||||
|
* [Document your new command](#document-your-new-command)
|
||||||
|
* [Implementing support for a new service](#implementing-support-for-a-new-service)
|
||||||
|
* [Create file for your new service](#create-file-for-your-new-service)
|
||||||
|
* [Copy template into your new file](#copy-template-into-your-new-file)
|
||||||
|
* [Implement your new service](#implement-your-new-service)
|
||||||
|
* [Add service to `services/pkg_init.go`](#add-command-to-servicespkg_initgo)
|
||||||
|
* [Add API key configuration value to `config.yaml` and `config.go` if necessary](#add-api-key-configuration-value-to-configyaml-and-configgo-if-necessary)
|
||||||
|
* [Document your new service](#document-your-new-service)
|
||||||
|
|
||||||
|
## Implementing a new command
|
||||||
|
Commands are the portion of MumbleDJ that allows users to interact with the bot. Here is a step-by-step guide on how to implement a new command:
|
||||||
|
|
||||||
|
### Create files for your new command
|
||||||
|
All commands possess their own `command.go` file and `command_test.go` file. These files must reside in the `commands` directory.
|
||||||
|
|
||||||
|
### Copy templates into your new files
|
||||||
|
Templates for both command implementations and command tests have been created to ensure consistency across the codebase. Please use these templates, they will make your implementation easier and will make the codebase much cleaner.
|
||||||
|
|
||||||
|
#### Command implementation template (`command.go`)
|
||||||
|
```go
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/yournewcommand.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/matthieugrieger/mumbledj/interfaces"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// YourNewCommand is a command... (put a short description of the command here)
|
||||||
|
type YourNewCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *YourNewCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.yournewcommand.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *YourNewCommand) Description() string {
|
||||||
|
return viper.GetString("commands.yournewcommand.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *YourNewCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.yournewcommand.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *YourNewCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Command test suite template (`command_test.go`)
|
||||||
|
```go
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/yournewcommand_test.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumbleffmpeg"
|
||||||
|
"github.com/matthieugrieger/mumbledj/bot"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type YourNewCommandTestSuite struct {
|
||||||
|
Command YourNewCommand
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *YourNewCommandTestSuite) SetupSuite() {
|
||||||
|
DJ = bot.NewMumbleDJ()
|
||||||
|
bot.DJ = DJ
|
||||||
|
|
||||||
|
viper.Set("commands.yournewcommand.aliases", []string{"yournewcommand", "c"})
|
||||||
|
viper.Set("commands.yournewcommand.description", "yournewcommand")
|
||||||
|
viper.Set("commands.yournewcommand.is_admin", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *YourNewCommandTestSuite) SetupTest() {
|
||||||
|
DJ.Queue = bot.NewQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *YourNewCommandTestSuite) TestAliases() {
|
||||||
|
suite.Equal([]string{"yournewcommand", "c"}, suite.Command.Aliases())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *YourNewCommandTestSuite) TestDescription() {
|
||||||
|
suite.Equal("yournewcommand", suite.Command.Description())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *YourNewCommandTestSuite) TestIsAdminCommand() {
|
||||||
|
suite.False(suite.Command.IsAdminCommand())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement more tests here as necessary! It may be helpful to take a look
|
||||||
|
// at the stretchr/testify documentation:
|
||||||
|
// https://github.com/stretchr/testify
|
||||||
|
// Remove this comment before sending a pull request.
|
||||||
|
|
||||||
|
func TestYourNewCommandTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(YourNewCommandTestSuite))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implement your new command
|
||||||
|
Now the fun starts! Write the implementation for your command in the `Execute()` method. Then, write tests for your new command, making sure to test each possible execution flow of your command.
|
||||||
|
|
||||||
|
For writing the implementation and unit tests for your new command, it may be helpful to [look at previously created commands](https://github.com/matthieugrieger/mumbledj/blob/master/commands).
|
||||||
|
|
||||||
|
**Make sure to rename the example names to represent your new command!**
|
||||||
|
|
||||||
|
### Add command to `commands/pkg_init.go`
|
||||||
|
`commands/pkg_init.go` contains a slice of enabled commands. If you do not put your command in this slice, your command will not be enabled.
|
||||||
|
|
||||||
|
**Please keep the commands in alphabetical order!**
|
||||||
|
|
||||||
|
### Add necessary configuration values to `config.yaml` and `config.go`
|
||||||
|
Go to `config.yaml` and `bot/config.go` and add the necessary configuration values for your new command.
|
||||||
|
|
||||||
|
**Please keep the commands in alphabetical order!**
|
||||||
|
|
||||||
|
### Regenerate `bindata.go`
|
||||||
|
This step is very easy, but is very important. This allows the bot to store a copy of the new `config.yaml` internally and use it to write to disk.
|
||||||
|
|
||||||
|
Simply execute `make bindata` and this step will be taken care of!
|
||||||
|
|
||||||
|
### Document your new command
|
||||||
|
Make sure to put information in `README.md` about your new command. It would be a shame for your new command to go unnoticed!
|
||||||
|
|
||||||
|
**Please keep the commands in alphabetical order!**
|
||||||
|
|
||||||
|
## Implementing support for a new service
|
||||||
|
Services are the portion of MumbleDJ that allows the bot to interact with various media services. Here is a step-by-step guide on how to implement support for a new service:
|
||||||
|
|
||||||
|
### Create file for your new service
|
||||||
|
All services possess their own `service.go` file. This file must reside in the `services` directory.
|
||||||
|
|
||||||
|
### Copy template into your new file
|
||||||
|
A template for service implementations has been created to ensure consistency across the codebase. Please use this template, it will make your implementation easier and will make the codebase much cleaner.
|
||||||
|
|
||||||
|
```go
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* services/yournewservice.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/antonholmquist/jason"
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/matthieugrieger/mumbledj/bot"
|
||||||
|
"github.com/matthieugrieger/mumbledj/interfaces"
|
||||||
|
)
|
||||||
|
|
||||||
|
// YourNewService is a... (description here)
|
||||||
|
type YourNewService struct {
|
||||||
|
*GenericService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewYourNewServiceService returns an initialized YourNewService service object.
|
||||||
|
func NewYourNewServiceService() *YourNewService {
|
||||||
|
return &YourNewService{
|
||||||
|
&GenericService{
|
||||||
|
ReadableName: "Your new service",
|
||||||
|
Format: "bestaudio",
|
||||||
|
TrackRegex: []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`regex for track URLs in your new service`),
|
||||||
|
},
|
||||||
|
PlaylistRegex: []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`regex for playlist URLs in your new service`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckAPIKey performs a test API call with the API key
|
||||||
|
// provided in the configuration file to determine if the
|
||||||
|
// service should be enabled.
|
||||||
|
func (yn *YourNewService) CheckAPIKey() error {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTracks uses the passed URL to find and return
|
||||||
|
// tracks associated with the URL. An error is returned
|
||||||
|
// if any error occurs during the API call.
|
||||||
|
func (yn *YourNewService) GetTracks(url string, submitter *gumble.User) ([]interfaces.Track, error) {
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implement your new service
|
||||||
|
Now the fun starts! Implement `CheckAPIKey()` and `GetTracks()`.
|
||||||
|
|
||||||
|
For writing the implementation for your new service, it may be helpful to [look at previously created service wrappers](https://github.com/matthieugrieger/mumbledj/blob/master/services).
|
||||||
|
|
||||||
|
**Make sure to rename the example names to represent your new service!**
|
||||||
|
|
||||||
|
### Add service to `services/pkg_init.go`
|
||||||
|
`services/pkg_init.go` contains a slice of enabled services. If you do not put your service in this slice, your service will not be enabled.
|
||||||
|
|
||||||
|
**Please keep the services in alphabetical order!**
|
||||||
|
|
||||||
|
### Add API key configuration value to `config.yaml` and `config.go` if necessary
|
||||||
|
Some services will require an API key for users to interact with their service. If an API key is required for your service, add it to the configuration file and `config.go` and run `make bindata` to regenerate the `bindata.go` file.
|
||||||
|
|
||||||
|
### Document your new service
|
||||||
|
In sections of `README.md` that describe which services are supported, add your new service to the list. It would be a shame for your new service to go unnoticed!
|
||||||
|
|
||||||
|
Also, if your service requires an API key, make sure to document the steps to retrieve an API key in the "Requirements" section of the `README`.
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2014, 2015 Matthieu Grieger
|
Copyright (c) 2016 Matthieu Grieger
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
38
Makefile
38
Makefile
|
@ -1,22 +1,26 @@
|
||||||
|
dirs = ./interfaces/... ./commands/... ./services/... ./bot/... .
|
||||||
|
|
||||||
all: mumbledj
|
all: mumbledj
|
||||||
|
|
||||||
mumbledj: main.go commands.go parseconfig.go strings.go service.go youtube_dl.go service_youtube.go service_soundcloud.go service_mixcloud.go songqueue.go cache.go
|
mumbledj: ## Default action. Builds MumbleDJ.
|
||||||
go get github.com/karmakaze/goop
|
@env GO15VENDOREXPERIMENT="1" go build .
|
||||||
rm -rf Goopfile.lock
|
|
||||||
goop install
|
|
||||||
goop go build
|
|
||||||
|
|
||||||
clean:
|
test: ## Runs unit tests for MumbleDJ.
|
||||||
rm -f mumbledj*
|
@env GO15VENDOREXPERIMENT="1" go test $(dirs)
|
||||||
|
|
||||||
install:
|
clean: ## Removes compiled MumbleDJ binaries.
|
||||||
mkdir -p ~/.mumbledj/config
|
@rm -f mumbledj*
|
||||||
mkdir -p ~/.mumbledj/songs
|
|
||||||
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
|
|
||||||
sed -i 's/YouTube = \"/YouTube = \"'$(YOUTUBE_API_KEY)'/' ~/.mumbledj/config/mumbledj.gcfg
|
|
||||||
sed -i 's/SoundCloud = \"/SoundCloud = \"'$(SOUNDCLOUD_API_KEY)'/' ~/.mumbledj/config/mumbledj.gcfg
|
|
||||||
if [ -d ~/bin ]; then cp -f mumbledj* ~/bin/mumbledj; else sudo cp -f mumbledj* /usr/local/bin/mumbledj; fi;
|
|
||||||
|
|
||||||
build:
|
install: ## Copies MumbleDJ binary to /usr/local/bin for easy execution.
|
||||||
goop go build
|
@cp -f mumbledj* /usr/local/bin/mumbledj
|
||||||
|
|
||||||
|
dist: ## Performs cross-platform builds via gox for multiple Linux platforms.
|
||||||
|
@go get -u github.com/mitchellh/gox
|
||||||
|
@gox -cgo -osarch="linux/amd64 linux/386"
|
||||||
|
|
||||||
|
bindata: ## Regenerates bindata.go with an updated configuration file.
|
||||||
|
@go get -u github.com/jteeuwen/go-bindata/...
|
||||||
|
@go-bindata config.yaml
|
||||||
|
|
||||||
|
help: ## Shows this helptext.
|
||||||
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
415
README.md
415
README.md
|
@ -1,190 +1,329 @@
|
||||||
MumbleDJ
|
|
||||||
========
|
|
||||||
**A Mumble bot that plays music fetched from YouTube, SoundCloud, and Mixcloud.**
|
|
||||||
|
|
||||||
* [Usage](#usage)
|
<h1 align="center">MumbleDJ</h1>
|
||||||
|
<p align="center"><b>A Mumble bot that plays audio fetched from various media websites.</b></p>
|
||||||
|
<p align="center"><a href="https://travis-ci.org/matthieugrieger/mumbledj"><img src="https://img.shields.io/travis/matthieugrieger/mumbledj.svg?style=flat-square"/></a> <a href="https://raw.githubusercontent.com/matthieugrieger/mumbledj/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square"/></a> <a href="https://github.com/matthieugrieger/mumbledj/releases"><img src="https://img.shields.io/github/release/matthieugrieger/mumbledj.svg?style=flat-square"/></a></p>
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
* [Features](#features)
|
* [Features](#features)
|
||||||
* [Commands](#commands)
|
|
||||||
* [Installation](#installation)
|
* [Installation](#installation)
|
||||||
* [YouTube API Keys](#youtube-api-keys)
|
* [Requirements](#requirements)
|
||||||
* [SoundCloud API Keys](#soundcloud-api-keys)
|
* [YouTube API Key](#youtube-api-key)
|
||||||
* [Setup Guide](#setup-guide)
|
* [SoundCloud API Key](#soundcloud-api-key)
|
||||||
* [Update Guide](#update-guide)
|
* [Via `go install`](#via-go-install-recommended)
|
||||||
* [Troubleshooting](#troubleshooting)
|
* [Pre-compiled Binaries](#pre-compiled-binaries-easiest)
|
||||||
|
* [From Source](#from-source)
|
||||||
|
* [Usage](#usage)
|
||||||
|
* [Commands](#commands)
|
||||||
|
* [Contributing](#contributing)
|
||||||
* [Author](#author)
|
* [Author](#author)
|
||||||
* [License](#license)
|
* [License](#license)
|
||||||
* [Thanks](#thanks)
|
* [Thanks](#thanks)
|
||||||
|
|
||||||
## USAGE
|
## Features
|
||||||
`$ mumbledj -server=localhost -port=64738 -username=MumbleDJ -password="hunter2"`
|
* Plays audio from many media websites, including YouTube, SoundCloud, and Mixcloud.
|
||||||
|
|
||||||
All commandline parameters are optional. Below are descriptions of all the available options:
|
|
||||||
|
|
||||||
* `-server`: The address of the Mumble server. Defaults to localhost.
|
|
||||||
* `-port`: The port number of the Mumble server. Defaults to 64738.
|
|
||||||
* `-username`: The username for the bot. Defaults to MumbleDJ.
|
|
||||||
* `-password`: The password for the Mumble server, if exists. Defaults to no password.
|
|
||||||
* `-channel`: The channel the bot enters after connecting to the Mumble server. Defaults to root.
|
|
||||||
* `-cert`: Path to user PEM certificate. Defaults to no cert.
|
|
||||||
* `-key`: Path to user PEM key. Defaults to no key.
|
|
||||||
* `-insecure`: If included, the bot will not check the certs for the server. Try using this commandline flag if you are having connection issues.
|
|
||||||
* `-accesstokens`: List of access tokens for the bot separated by spaces. Defaults to no access tokens.
|
|
||||||
* `-version`: Outputs the version of MumbleDJ currently being used and then exits.
|
|
||||||
|
|
||||||
## FEATURES
|
|
||||||
* Plays audio from YouTube, SoundCloud, and Mixcloud!
|
|
||||||
* Supports playlists and individual videos/tracks.
|
* Supports playlists and individual videos/tracks.
|
||||||
* Displays thumbnail, title, duration, submitter, and playlist title (if exists) when a new song is played.
|
* Displays metadata in the text chat whenever a new track starts playing.
|
||||||
* Incredible customization options. Nearly everything is able to be tweaked in `~/.mumbledj/mumbledj.gcfg`.
|
* Incredibly customizable. Nearly everything is able to be tweaked via configuration files (by default located at `$HOME/.config/mumbledj/config.yaml`).
|
||||||
* A large array of [commands](#commands) that perform a wide variety of functions.
|
* A large array of [commands](#commands) that perform a wide variety of functions.
|
||||||
* Built-in vote-skipping.
|
* Built-in vote-skipping.
|
||||||
* Built-in caching system (disabled by default).
|
* Built-in caching system (disabled by default).
|
||||||
|
* Built-in play/pause/volume control.
|
||||||
|
|
||||||
## COMMANDS
|
## Installation
|
||||||
These are all of the chat commands currently supported by MumbleDJ. All command names and command prefixes may be changed in `~/.mumbledj/config/mumbledj.gcfg`.
|
**IMPORTANT NOTE:** MumbleDJ is only tested and developed for Linux systems. Support will not be given for non-Linux systems if problems are encountered.
|
||||||
|
|
||||||
Command | Description | Arguments | Admin | Example
|
### Requirements
|
||||||
--------|-------------|-----------|-------|--------
|
**All MumbleDJ installations must also have the following installed:**
|
||||||
**add** | Adds audio from a url to the song queue. If no songs are currently in the queue, the audio will begin playing immediately. Playlists may also be added using this command. The maximum amount of songs that can be added from a playlist is specified in `mumbledj.gcfg`. | youtube_video_url OR youtube_playlist_url OR soundcloud_track_url OR soundcloud_playlist_url | No | `!add https://www.youtube.com/watch?v=5xfEr2Oxdys`
|
* [`youtube-dl`](https://rg3.github.io/youtube-dl/download.html)
|
||||||
**addnext** | Adds audio from a url to the song queue after the current song. If no songs are currently in the queue, the audio will begin playing immediately. Playlists may also be added using this command. The maximum amount of songs that can be added from a playlist is specified in `mumbledj.gcfg`. | youtube_video_url OR youtube_playlist_url OR soundcloud_track_url OR soundcloud_playlist_url | Yes | `!addnext https://www.youtube.com/watch?v=5xfEr2Oxdys`
|
* [`ffmpeg`](https://ffmpeg.org) OR [`avconv`](https://libav.org)
|
||||||
**skip**| Submits a vote to skip the current song. Once the skip ratio target (specified in `mumbledj.gcfg`) is met, the song will be skipped and the next will start playing. Each user may only submit one skip per song. | None | No | `!skip`
|
* [`aria2`](https://aria2.github.io/) if you plan on using services that throttle download speeds (like Mixcloud)
|
||||||
**skipplaylist** | Submits a vote to skip the current playlist. Once the skip ratio target (specified in `mumbledj.gcfg`) is met, the playlist will be skipped and the next song/playlist will start playing. Each user may only submit one skip per playlist. | None | No | `!skipplaylist`
|
|
||||||
**forceskip** | An admin command that forces a song skip. | None | Yes | `!forceskip`
|
|
||||||
**forceskipplaylist** | An admin command that forces a playlist skip. | None | Yes | `!forceskipplaylist`
|
|
||||||
**shuffle** | An admin command that shuffles the current queue. | None | Yes | `!shuffle`
|
|
||||||
**shuffleon** | An admin command that enables auto shuffling. | None | Yes | `!shuffleon`
|
|
||||||
**shuffleoff** | An admin command that disables auto shuffling. | None | Yes | `!shuffleoff`
|
|
||||||
**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`
|
|
||||||
**move** | Moves MumbleDJ into channel if it exists. | Channel | Yes | `!move Music`
|
|
||||||
**joinme** | Moves MumbleDJ into your current channel if not playing audio to someone else. | None | Yes | `!joinme`
|
|
||||||
**reload** | Reloads `mumbledj.gcfg` to retrieve updated configuration settings. | None | Yes | `!reload`
|
|
||||||
**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`
|
|
||||||
**nextsong** | Outputs the title and name of the submitter of the next song in the queue if it exists. | None | No | `!nextsong`
|
|
||||||
**currentsong** | Outputs the title and name of the submitter of the song currently playing. | None | No | `!currentsong`
|
|
||||||
**listsongs** | Outputs a list of the songs currently in the queue. | None or desired number of songs to list | No | `!listsongs`
|
|
||||||
**setcomment** | Sets the comment for the bot. If no argument is given, the current comment will be removed. | None OR new_comment | Yes | `!setcomment Hello! I am a bot. Type !help for the available commands.`
|
|
||||||
**numcached** | Outputs the number of songs currently cached on disk. | None | Yes | `!numcached`
|
|
||||||
**cachesize** | Outputs the total file size of the cache in MB. | None | Yes | `!cachesize`
|
|
||||||
**kill** | Safely cleans the bot environment and disconnects from the server. Please use this command to stop the bot instead of force closing, as the kill command deletes any remaining songs in the `~/.mumbledj/songs` directory. | None | Yes | `!kill`
|
|
||||||
|
|
||||||
|
**If installing via `go install` or from source, the following must be installed:**
|
||||||
|
* [Go 1.5+](https://golang.org)
|
||||||
|
* __NOTE__: Extra installation steps are required for a working Go installation. Once Go is installed, type `go help gopath` for more information.
|
||||||
|
|
||||||
|
#### YouTube API Key
|
||||||
|
A YouTube API key must be present in your configuration file in order to use the YouTube service within the bot. Below is a guide for retrieving an API key:
|
||||||
|
|
||||||
|
**1)** Navigate to the [Google Developers Console](https://console.developers.google.com) and sign in with your Google account, or create one if you haven't already.
|
||||||
## INSTALLATION
|
|
||||||
|
|
||||||
###YOUTUBE API KEYS
|
|
||||||
Effective April 20th, 2015, all requests to YouTube's API must use v3 of their API. Unfortunately, this means that all those who install an instance of the bot on their server must create their own API key to use with the bot. Below is a guide of the steps you must take to get proper YouTube support.
|
|
||||||
|
|
||||||
**Important:** MumbleDJ will simply not work anymore if you do not follow these steps and create a YouTube API key.
|
|
||||||
|
|
||||||
**1)** Navigate to the [Google Developers Console](https://console.developers.google.com) and sign in to your Google account or create one if you haven't already.
|
|
||||||
|
|
||||||
**2)** Click the "Create Project" button and give your project a name. It doesn't matter what you set your project name to. Once you have a name click the "Create" button. You should be redirected to your new project once it's ready.
|
**2)** Click the "Create Project" button and give your project a name. It doesn't matter what you set your project name to. Once you have a name click the "Create" button. You should be redirected to your new project once it's ready.
|
||||||
|
|
||||||
**3)** Click on "APIs & auth" on the sidebar, and then click APIs. Under the "YouTube APIs" header, click "YouTube Data API". Click on the "Enable API" button.
|
**3)** Click on "APIs & auth" on the sidebar, and then click APIs. Under the "YouTube APIs" header, click "YouTube Data API". Click on the "Enable API" button.
|
||||||
|
|
||||||
**4)** Click on the "Credentials" option underneath "APIs & auth" on the sidebar. Underneath "Public API access" click on "Create new Key". Click the "Server key" option.
|
**4)** Click on the "Credentials" option underneath "APIs & auth" on the sidebar. Underneath "Public API access" click on "Create New Key". Choose the "Server key" option.
|
||||||
|
|
||||||
**5)** Add the IP address of the machine MumbleDJ will run on in the box that appears. Click "Create".
|
**5)** Add the IP address of the machine MumbleDJ will run on in the box that appears (this is optional, but improves security). Click "Create".
|
||||||
|
|
||||||
**6)** You should now see that an API key has been generated, make a note of it.
|
**6)** You should now see that an API key has been generated. Copy/paste this API key into the configuration file located at `$HOME/.config/mumbledj/mumbledj.yaml`.
|
||||||
|
|
||||||
###SOUNDCLOUD API KEYS
|
#### SoundCloud API Key
|
||||||
A SoundCloud API key is required for SoundCloud integration. If no SoundCloud API key is found, then the service will be disabled (YouTube links will still work however).
|
A SoundCloud client ID must be present in your configuration file in order to use the SoundCloud service within the bot. Below is a guide for retrieving a client ID:
|
||||||
|
|
||||||
**1)** Login/signup for a SoundCloud account on [https://soundcloud.com](https://soundcloud.com)
|
**1)** Login/sign up for a SoundCloud account on https://soundcloud.com.
|
||||||
|
|
||||||
**2)** Now to get the API key create a new app here: [http://soundcloud.com/you/apps/new](http://soundcloud.com/you/apps/new)
|
**2)** Create a new app: https://soundcloud.com/you/apps/new.
|
||||||
|
|
||||||
**3)** Make a note of the Client ID (not the Client Secret).
|
**3)** You should now see that a client ID has been generated. Copy/paste this ID (NOT the client secret) into the configuration file located at `$HOME/.config/mumbledj/mumbledj.yaml`.
|
||||||
|
|
||||||
|
|
||||||
**NOTE:** If you get errors when trying to play SoundCloud audio, make sure to update `youtube-dl` with `youtube-dl -U`!
|
### Via `go install` (recommended)
|
||||||
|
After verifying that the [requirements](#requirements) are installed, simply issue the following command:
|
||||||
###SETUP GUIDE
|
|
||||||
**1)** Install and correctly configure [`Go`](https://golang.org/) (1.4 or higher). Specifically, make sure to follow [this guide](https://golang.org/doc/code.html) and set the `GOPATH` environment variable properly.
|
|
||||||
|
|
||||||
**2)** Install [`ffmpeg`](https://www.ffmpeg.org/) and [`mercurial`](http://mercurial.selenic.com/) if they are not already installed on your system. Also be sure that you have
|
|
||||||
[`opus`](http://www.opus-codec.org/) and its development headers installed on your system, as well as `openal` (check your distributions repo for the package name). If you want to use `avconv` from `libav` instead of `ffmpeg` you must make the necessary change in the configuration file.
|
|
||||||
|
|
||||||
**3)** Install [`youtube-dl`](https://github.com/rg3/youtube-dl#installation). It is recommended to install `youtube-dl` through the method described on the linked GitHub page, rather than installing through a distribution repository. This ensures that you get the most up-to-date version of `youtube-dl`.
|
|
||||||
|
|
||||||
**4)** If you wish to install MumbleDJ without any further root privileges, make sure that `~/bin` exists and is added to your `$PATH`. If this step is not done, the `Makefile` will place the MumbleDJ binary in `/usr/local/bin` instead, which requires root privileges.
|
|
||||||
|
|
||||||
**5)** Clone the `MumbleDJ` repository or [download the latest release](https://github.com/matthieugrieger/mumbledj/releases).
|
|
||||||
|
|
||||||
**6)** `cd` into the `MumbleDJ` repository directory and execute the following commands:
|
|
||||||
```
|
```
|
||||||
$ make
|
go install github.com/matthieugrieger/mumbledj
|
||||||
$ make install
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**7)** Edit `~/.mumbledj/config/mumbledj.gcfg` to your liking, make sure to include your API keys! This file will be overwritten if the config file structure is changed in a commit, but a backup is always stored at
|
This will place a binary in `$GOPATH/bin` that can be used to start the bot.
|
||||||
`~/.mumbledj/config/mumbledj_backup.gcfg`.
|
|
||||||
|
|
||||||
**8)** Execute the command shown at the top of this `README` document with your credentials, and the bot should be up and running!
|
**NOTE:** If using Go 1.5, you MUST execute the following for `go install` to work:
|
||||||
|
|
||||||
**Recommended, but not required:** Set `opusthreshold=0` in `/etc/mumble-server.ini` or `/etc/murmur.ini`. This will force the server to always use the Opus audio codec, which is the only codec that MumbleDJ supports.
|
|
||||||
|
|
||||||
###UPDATE GUIDE
|
|
||||||
**1)** `git pull` or [download the latest release](https://github.com/matthieugrieger/mumbledj/releases).
|
|
||||||
|
|
||||||
**2)** Issue the following commands within your updated MumbleDJ directory:
|
|
||||||
```
|
```
|
||||||
$ make clean
|
export GO15VENDOREXPERIMENT=1
|
||||||
$ make
|
|
||||||
$ make install
|
|
||||||
```
|
```
|
||||||
**NOTE**: It is *very* important that you use `make` instead of `make build` when updating MumbleDJ as the first option will grab the latest updates from MumbleDJ's dependencies.
|
|
||||||
|
|
||||||
**3)** Check to make sure your configuration in `~/.mumbledj/config/mumbledj.gcfg` is the same as before. If it is back to the default, a backup should have been created at `~/.mumbledj/config/mumbledj_backup.gcfg` so you can copy the values back over.
|
### Pre-compiled Binaries (easiest)
|
||||||
|
Pre-compiled binaries are provided for convenience. Overall, I do not recommend using these unless you cannot get `go install` to work properly. Binaries compiled on your own machine are likely more efficient as these binaries are cross-compiled from a 64-bit Linux system.
|
||||||
|
|
||||||
## TROUBLESHOOTING
|
After verifying that the [requirements](#requirements) are installed, simply visit the [releases page](https://github.com/matthieugrieger/mumbledj/releases) and download the appropriate binary for your platform.
|
||||||
**YouTube videos downloads work when using `youtube-dl` but not within MumbleDJ.**
|
|
||||||
|
|
||||||
This is likely related to how you set up your Google account for the YouTube API. Specifically, make sure you you try using an IPv4 server address in the list of allowed IPs if you were using IPv6 previously.
|
### From Source
|
||||||
|
First, clone the MumbleDJ repository to your machine:
|
||||||
|
```
|
||||||
|
git clone https://github.com/matthieugrieger/mumbledj.git
|
||||||
|
```
|
||||||
|
|
||||||
**Whenever the `!add` command is used I receive the following message in chat: "The audio download for this video failed. YouTube has likely not generated the audio files for this video yet. Skipping to the next song!"**
|
Install the required software as described in the [requirements section](#requirements), and execute the following:
|
||||||
|
```
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
First, make sure you have `youtube-dl` installed and it is the latest version. MumbleDJ makes use of `youtube-dl`'s `--` commandline argument which is not supported in older versions.
|
This will place a compiled `mumbledj` binary in the cloned directory if successful. If you would like to make the binary more accessible by adding it to `/usr/local/bin`, simply execute the following:
|
||||||
|
```
|
||||||
|
sudo make install
|
||||||
|
```
|
||||||
|
|
||||||
If this doesn't fix the issue, try the following fix from [@MrKrucible](https://github.com/MrKrucible):
|
## Usage
|
||||||
|
MumbleDJ is a compiled program that is executed via a terminal.
|
||||||
|
|
||||||
>I fixed it by following the instructions here to set default arguments: https://github.com/rg3/youtube-dl/blob/master/README.md#configuration
|
Here is an example helptext that gives you a feel for the various commandline arguments you can give MumbleDJ:
|
||||||
|
|
||||||
>For the lazy...
|
```
|
||||||
>- 1. First make ```~/.config/youtube-dl/``` and create a file named ```config```.
|
NAME:
|
||||||
>- 2. Then put ```--force-ipv4``` into the config. Nothing else needs to be in there unless you want to add more arguments.
|
MumbleDJ - A Mumble bot that plays audio from various media sites.
|
||||||
|
|
||||||
**I receive the following error when compiling MumbleDJ: "undefined: tls.DialWithDialer"**
|
USAGE:
|
||||||
|
mumbledj [global options] command [command options] [arguments...]
|
||||||
|
|
||||||
|
VERSION:
|
||||||
|
3.0.0-alpha
|
||||||
|
|
||||||
|
COMMANDS:
|
||||||
|
GLOBAL OPTIONS:
|
||||||
|
--config value, -c value location of MumbleDJ configuration file (default: "$HOME/.config/mumbledj/mumbledj.yaml")
|
||||||
|
--server value, -s value address of Mumble server to connect to (default: "127.0.0.1")
|
||||||
|
--port value, -o value port of Mumble server to connect to (default: "64738")
|
||||||
|
--username value, -u value username for the bot (default: "MumbleDJ")
|
||||||
|
--password value, -p value password for the Mumble server
|
||||||
|
--channel value, -n value channel the bot enters after connecting to the Mumble server
|
||||||
|
--cert value, -e value path to PEM certificate
|
||||||
|
--key value, -k value path to PEM key
|
||||||
|
--accesstokens value, -a value list of access tokens separated by spaces
|
||||||
|
--insecure, -i if present, the bot will not check Mumble certs for consistency
|
||||||
|
--debug, -d if present, all debug messages will be shown
|
||||||
|
--help, -h show help
|
||||||
|
--version, -v print the version
|
||||||
|
|
||||||
This issue is caused by having an outdated version of Go. Make sure you are using the latest available version of Go.
|
```
|
||||||
|
|
||||||
**I can't get MumbleDJ to compile correctly under `gccgo`**
|
## Commands
|
||||||
|
|
||||||
Unfortunately MumbleDJ likely will not work with `gccgo`. MumbleDJ is developed and tested on vanilla Go.
|
### add
|
||||||
|
* __Description__: Adds a track or playlist from a media site to the queue.
|
||||||
|
* __Default Aliases__: add, a
|
||||||
|
* __Arguments__: (Required) URL(s) to a track or playlist from a supported media site.
|
||||||
|
* __Admin-only by default__: No
|
||||||
|
* __Example__: `!add https://www.youtube.com/watch?v=KQY9zrjPBjo`
|
||||||
|
|
||||||
**I receive the following message when compiling MumbleDJ: "local.h:6:19: error: AL/alc.h: No such file or directory"**
|
### addnext
|
||||||
|
* __Description__: Adds a track or playlist from a media site as the next item in the queue.
|
||||||
|
* __Default Aliases__: addnext, an
|
||||||
|
* __Arguments__: (Required) URL(s) to a track or playlist from a supported media site.
|
||||||
|
* __Admin-only by default__: Yes
|
||||||
|
* __Example__: `!addnext https://www.youtube.com/watch?v=KQY9zrjPBjo`
|
||||||
|
|
||||||
Don't worry about it. The compilation went through successfully, OpenAL is not needed by the bot.
|
### cachesize
|
||||||
|
* __Description__: Outputs the file size of the cache in MiB if caching is enabled.
|
||||||
|
* __Default Aliases__: cachesize, cs
|
||||||
|
* __Arguments__: None
|
||||||
|
* __Admin-only by default__: Yes
|
||||||
|
* __Example__: `!cachesize`
|
||||||
|
|
||||||
**I receive the following message when building MumbleDJ: "cannot create \<nil\>/go.o: No such file or directory"**
|
### currenttrack
|
||||||
|
* __Description__: Outputs information about the current track in the queue if one exists.
|
||||||
|
* __Default Aliases__: currenttrack, currentsong, current
|
||||||
|
* __Arguments__: None
|
||||||
|
* __Admin-only by default__: No
|
||||||
|
* __Example__: `!currenttrack`
|
||||||
|
|
||||||
Execute the following before building:
|
### forceskip
|
||||||
|
* __Description__: Immediately skips the current track.
|
||||||
|
* __Default Aliases__: forceskip, fs
|
||||||
|
* __Arguments__: None
|
||||||
|
* __Admin-only by default__: Yes
|
||||||
|
* __Example__: `!forceskip`
|
||||||
|
|
||||||
```export TMPDIR=/tmp```
|
### forceskipplaylist
|
||||||
|
* __Description__: Immediately skips the current playlist.
|
||||||
|
* __Default Aliases__: forceskipplaylist, fsp
|
||||||
|
* __Arguments__: None
|
||||||
|
* __Admin-only by default__: Yes
|
||||||
|
* __Example__: `!forceskipplaylist`
|
||||||
|
|
||||||
|
### help
|
||||||
|
* __Description__: Outputs a list of available commands and their descriptions.
|
||||||
|
* __Default Aliases__: help, h
|
||||||
|
* __Arguments__: None
|
||||||
|
* __Admin-only by default__: No
|
||||||
|
* __Example__: `!help`
|
||||||
|
|
||||||
## AUTHOR
|
### joinme
|
||||||
[Matthieu Grieger](http://matthieugrieger.com)
|
* __Description__: Moves MumbleDJ into your current channel if not playing audio to someone else.
|
||||||
|
* __Default Aliases__: joinme, join
|
||||||
|
* __Arguments__: None
|
||||||
|
* __Admin-only by default__: Yes
|
||||||
|
* __Example__: `!joinme`
|
||||||
|
|
||||||
## LICENSE
|
### kill
|
||||||
|
* __Description__: Stops the bot and cleans its cache directory.
|
||||||
|
* __Default Aliases__: kill, k
|
||||||
|
* __Arguments__: None
|
||||||
|
* __Admin-only by default__: Yes
|
||||||
|
* __Example__: `!kill`
|
||||||
|
|
||||||
|
### listtracks
|
||||||
|
* __Description__: Outputs a list of the tracks currently in the queue.
|
||||||
|
* __Default Aliases__: listtracks, listsongs, list, l
|
||||||
|
* __Arguments__: (Optional) Number of tracks to list
|
||||||
|
* __Admin-only by default__: No
|
||||||
|
* __Example__: `!listtracks 10`
|
||||||
|
|
||||||
|
### move
|
||||||
|
* __Description__: Moves the bot into the Mumble channel provided via argument.
|
||||||
|
* __Default Aliases__: move, m
|
||||||
|
* __Arguments__: (Required) Mumble channel to move the bot into
|
||||||
|
* __Admin-only by default__: Yes
|
||||||
|
* __Example__: `!move Music`
|
||||||
|
|
||||||
|
### nexttrack
|
||||||
|
* __Description__: Outputs information about the next track in the queue if one exists.
|
||||||
|
* __Default Aliases__: nexttrack, nextsong, next
|
||||||
|
* __Arguments__: None
|
||||||
|
* __Admin-only by default__: No
|
||||||
|
* __Example__: `!nexttrack`
|
||||||
|
|
||||||
|
### numcached
|
||||||
|
* __Description__: Outputs the number of tracks cached on disk if caching is enabled.
|
||||||
|
* __Default Aliases__: numcached, nc
|
||||||
|
* __Arguments__: None
|
||||||
|
* __Admin-only by default__: Yes
|
||||||
|
* __Example__: `!numcached`
|
||||||
|
|
||||||
|
### numtracks
|
||||||
|
* __Description__: Outputs the number of tracks currently in the queue.
|
||||||
|
* __Default Aliases__: numtracks, numsongs, nt
|
||||||
|
* __Arguments__: None
|
||||||
|
* __Admin-only by default__: No
|
||||||
|
* __Example__: `!numtracks`
|
||||||
|
|
||||||
|
### pause
|
||||||
|
* __Description__: Pauses audio playback.
|
||||||
|
* __Default Aliases__: pause
|
||||||
|
* __Arguments__: None
|
||||||
|
* __Admin-only by default__: No
|
||||||
|
* __Example__: `!pause`
|
||||||
|
|
||||||
|
### reload
|
||||||
|
* __Description__: Reloads the configuration file.
|
||||||
|
* __Default Aliases__: reload, r
|
||||||
|
* __Arguments__: None
|
||||||
|
* __Admin-only by default__: Yes
|
||||||
|
* __Example__: `!reload`
|
||||||
|
|
||||||
|
### reset
|
||||||
|
* __Description__: Resets the queue by removing all queue items.
|
||||||
|
* __Default Aliases__: reset, re
|
||||||
|
* __Arguments__: None
|
||||||
|
* __Admin-only by default__: Yes
|
||||||
|
* __Example__: `!reset`
|
||||||
|
|
||||||
|
### resume
|
||||||
|
* __Description__: Resumes audio playback.
|
||||||
|
* __Default Aliases__: resume
|
||||||
|
* __Arguments__: None
|
||||||
|
* __Admin-only by default__: No
|
||||||
|
* __Example__: `!pause`
|
||||||
|
|
||||||
|
### setcomment
|
||||||
|
* __Description__: Sets the comment displayed next to MumbleDJ's username in Mumble. If the argument is left empty, the current comment is removed.
|
||||||
|
* __Default Aliases__: setcomment, comment, sc
|
||||||
|
* __Arguments__: (Optional) New comment
|
||||||
|
* __Admin-only by default__: Yes
|
||||||
|
* __Example__: `!setcomment Hello! I'm a bot. Beep boop.`
|
||||||
|
|
||||||
|
### shuffle
|
||||||
|
* __Description__: Randomizes the tracks currently in the queue.
|
||||||
|
* __Default Aliases__: shuffle, shuf, sh
|
||||||
|
* __Arguments__: None
|
||||||
|
* __Admin-only by default__: Yes
|
||||||
|
* __Example__: `!shuffle`
|
||||||
|
|
||||||
|
### skip
|
||||||
|
* __Description__: Places a vote to skip the current track.
|
||||||
|
* __Default Aliases__: skip, s
|
||||||
|
* __Arguments__: None
|
||||||
|
* __Admin-only by default__: No
|
||||||
|
* __Example__: `!skip`
|
||||||
|
|
||||||
|
### skipplaylist
|
||||||
|
* __Description__: Places a vote to skip the current playlist.
|
||||||
|
* __Default Aliases__: skipplaylist, sp
|
||||||
|
* __Arguments__: None
|
||||||
|
* __Admin-only by default__: No
|
||||||
|
* __Example__: `!skipplaylist`
|
||||||
|
|
||||||
|
### toggleshuffle
|
||||||
|
* __Description__: Toggles permanent track shuffling on/off.
|
||||||
|
* __Default Aliases__: toggleshuffle, toggleshuf, togshuf, tsh
|
||||||
|
* __Arguments__: None
|
||||||
|
* __Admin-only by default__: Yes
|
||||||
|
* __Example__: `!toggleshuffle`
|
||||||
|
|
||||||
|
### version
|
||||||
|
* __Description__: Outputs the current version of MumbleDJ.
|
||||||
|
* __Default Aliases__: version, v
|
||||||
|
* __Arguments__: None
|
||||||
|
* __Admin-only by default__: No
|
||||||
|
* __Example__: `!version`
|
||||||
|
|
||||||
|
### volume
|
||||||
|
* __Description__: Changes the volume if an argument is provided, outputs the current volume otherwise.
|
||||||
|
* __Default Aliases__: volume, vol
|
||||||
|
* __Arguments__: (Optional) New volume
|
||||||
|
* __Admin-only by default__: No
|
||||||
|
* __Example__: `!volume 0.5`
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions to MumbleDJ are always welcome! Please see the [contribution guidelines](https://github.com/matthieugrieger/mumbledj/blob/master/CONTRIBUTING.md) for instructions and suggestions!
|
||||||
|
|
||||||
|
## Author
|
||||||
|
[Matthieu Grieger](https://github.com/matthieugrieger)
|
||||||
|
|
||||||
|
## License
|
||||||
```
|
```
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2014, 2015 Matthieu Grieger
|
Copyright (c) 2016 Matthieu Grieger
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
@ -205,10 +344,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
```
|
```
|
||||||
|
|
||||||
## THANKS
|
## Thanks
|
||||||
* All those who contribute to [Mumble](https://github.com/mumble-voip/mumble).
|
* [All those who contribute to Mumble](https://github.com/mumble-voip/mumble/graphs/contributors)
|
||||||
* [Tim Cooper](https://github.com/bontibon) for [gumble](https://github.com/layeh/gumble).
|
* [Tim Cooper](https://github.com/bontibon) for [gumble, gumbleffmpeg, and gumbleutil](https://github.com/layeh/gumble)
|
||||||
* [Ricardo Garcia](https://github.com/rg3) for [youtube-dl](https://github.com/rg3/youtube-dl).
|
* [Jeremy Saenz](https://github.com/codegangsta) for [cli](https://github.com/urfave/cli)
|
||||||
* [ScalingData](https://github.com/scalingdata) for [gcfg](https://github.com/scalingdata/gcfg).
|
* [Anton Holmquist](https://github.com/antonholmquist) for [jason](https://github.com/antonholmquist/jason)
|
||||||
* [Jason Moiron](https://github.com/jmoiron) for [jsonq](https://github.com/jmoiron/jsonq).
|
* [Stretchr, Inc.](https://github.com/stretchr) for [testify](https://github.com/stretchr/testify)
|
||||||
* [Nitrous.IO](https://github.com/nitrous-io) for [goop](https://github.com/nitrous-io/goop).
|
* [ChannelMeter](https://github.com/ChannelMeter) for [iso8601duration](https://github.com/ChannelMeter/iso8601duration)
|
||||||
|
* [Steve Francia](https://github.com/spf13) for [viper](https://github.com/spf13/viper)
|
||||||
|
* [Simon Eskildsen](https://github.com/Sirupsen) for [logrus](https://github.com/Sirupsen/logrus)
|
||||||
|
* [Mitchell Hashimoto](https://github.com/mitchellh) for [gox](https://github.com/mitchellh/gox)
|
||||||
|
* [Jim Teeuwen](https://github.com/jteeuwen) for [go-bindata](https://github.com/jteeuwen/go-bindata)
|
||||||
|
|
235
bindata.go
Normal file
235
bindata.go
Normal file
File diff suppressed because one or more lines are too long
144
bot/cache.go
Normal file
144
bot/cache.go
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* bot/cache.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SortFilesByAge is a type that holds file information for cached items for
|
||||||
|
// sorting.
|
||||||
|
type SortFilesByAge []os.FileInfo
|
||||||
|
|
||||||
|
// Len returns the length of the file slice.
|
||||||
|
func (a SortFilesByAge) Len() int {
|
||||||
|
return len(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap swaps two elements in the file slice.
|
||||||
|
func (a SortFilesByAge) Swap(i, j int) {
|
||||||
|
a[i], a[j] = a[j], a[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Less compares two file modification times to determine if one is less than
|
||||||
|
// the other. Returns true if the item in index i is older than the item in
|
||||||
|
// index j, false otherwise.
|
||||||
|
func (a SortFilesByAge) Less(i, j int) bool {
|
||||||
|
return time.Since(a[i].ModTime()) < time.Since(a[j].ModTime())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache keeps track of the filesize of the audio cache and
|
||||||
|
// provides methods for pruning the cache.
|
||||||
|
type Cache struct {
|
||||||
|
NumAudioFiles int
|
||||||
|
TotalFileSize int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCache creates an empty Cache and returns it.
|
||||||
|
func NewCache() *Cache {
|
||||||
|
return &Cache{
|
||||||
|
NumAudioFiles: 0,
|
||||||
|
TotalFileSize: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckDirectorySize checks the cache directory to determine if the filesize
|
||||||
|
// of the files within exceed the user-specified size limit. If so, the oldest
|
||||||
|
// files are cleared until it is no longer exceeding the limit.
|
||||||
|
func (c *Cache) CheckDirectorySize() {
|
||||||
|
const bytesInMiB int = 1048576
|
||||||
|
|
||||||
|
c.UpdateStatistics()
|
||||||
|
for c.TotalFileSize > int64(viper.GetInt("cache.maximum_size")*bytesInMiB) {
|
||||||
|
if err := c.DeleteOldest(); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStatistics updates the statistics relevant to the cache (number of
|
||||||
|
// audio files cached, total current size of the cache).
|
||||||
|
func (c *Cache) UpdateStatistics() {
|
||||||
|
c.NumAudioFiles, c.TotalFileSize = c.getCurrentStatistics()
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"num_audio_files": c.NumAudioFiles,
|
||||||
|
"total_file_size": c.TotalFileSize,
|
||||||
|
}).Infoln("Updated cache statistics.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanPeriodically loops forever, deleting expired cached audio files as necessary.
|
||||||
|
func (c *Cache) CleanPeriodically() {
|
||||||
|
for range time.Tick(time.Duration(viper.GetInt("cache.check_interval")) * time.Minute) {
|
||||||
|
logrus.Infoln("Checking cache for expired files...")
|
||||||
|
files, _ := ioutil.ReadDir(os.ExpandEnv(viper.GetString("cache.directory")))
|
||||||
|
for _, file := range files {
|
||||||
|
// It is safe to check the modification time because when audio files are
|
||||||
|
// played their modification time is updated. This ensures that audio
|
||||||
|
// files will not get deleted while they are playing, assuming a reasonable
|
||||||
|
// expiry time is set in the configuration.
|
||||||
|
hours := time.Since(file.ModTime()).Hours()
|
||||||
|
if hours >= viper.GetFloat64("cache.expire_time") {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"expired_file": file.Name(),
|
||||||
|
}).Infoln("Removing expired cache entry.")
|
||||||
|
os.Remove(fmt.Sprintf("%s/%s", os.ExpandEnv(viper.GetString("cache.directory")), file.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteOldest deletes the oldest file in the cache.
|
||||||
|
func (c *Cache) DeleteOldest() error {
|
||||||
|
files, _ := ioutil.ReadDir(os.ExpandEnv(viper.GetString("cache.directory")))
|
||||||
|
if len(files) > 0 {
|
||||||
|
sort.Sort(SortFilesByAge(files))
|
||||||
|
os.Remove(fmt.Sprintf("%s/%s", os.ExpandEnv(viper.GetString("cache.directory")), files[0].Name()))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("There are no files currently cached")
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAll deletes all cached audio files.
|
||||||
|
func (c *Cache) DeleteAll() error {
|
||||||
|
dir, err := os.Open(os.ExpandEnv(viper.GetString("cache.directory")))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer dir.Close()
|
||||||
|
names, err := dir.Readdirnames(-1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logrus.Infoln("Deleting all cached audio files...")
|
||||||
|
for _, name := range names {
|
||||||
|
err = os.RemoveAll(filepath.Join(os.ExpandEnv(viper.GetString("cache.directory")), name))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) getCurrentStatistics() (int, int64) {
|
||||||
|
var totalSize int64
|
||||||
|
files, _ := ioutil.ReadDir(os.ExpandEnv(viper.GetString("cache.directory")))
|
||||||
|
for _, file := range files {
|
||||||
|
totalSize += file.Size()
|
||||||
|
}
|
||||||
|
return len(files), totalSize
|
||||||
|
}
|
199
bot/config.go
Normal file
199
bot/config.go
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* bot/config.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetDefaultConfig sets default values for all configuration options.
|
||||||
|
func SetDefaultConfig() {
|
||||||
|
// API key defaults.
|
||||||
|
viper.SetDefault("api_keys.youtube", "")
|
||||||
|
viper.SetDefault("api_keys.soundcloud", "")
|
||||||
|
|
||||||
|
// General defaults.
|
||||||
|
viper.SetDefault("defaults.comment", "Hello! I am a bot. Type !help for a list of commands.")
|
||||||
|
viper.SetDefault("defaults.channel", "")
|
||||||
|
viper.SetDefault("defaults.player_command", "ffmpeg")
|
||||||
|
|
||||||
|
// Queue defaults.
|
||||||
|
viper.SetDefault("queue.track_skip_ratio", 0.5)
|
||||||
|
viper.SetDefault("queue.playlist_skip_ratio", 0.5)
|
||||||
|
viper.SetDefault("queue.max_track_duration", 0)
|
||||||
|
viper.SetDefault("queue.max_tracks_per_playlist", 50)
|
||||||
|
viper.SetDefault("queue.automatic_shuffle_on", false)
|
||||||
|
viper.SetDefault("queue.announce_new_tracks", true)
|
||||||
|
|
||||||
|
// Connection defaults.
|
||||||
|
viper.SetDefault("connection.address", "127.0.0.1")
|
||||||
|
viper.SetDefault("connection.port", 64738)
|
||||||
|
viper.SetDefault("connection.password", "")
|
||||||
|
viper.SetDefault("connection.username", "MumbleDJ")
|
||||||
|
viper.SetDefault("connection.insecure", false)
|
||||||
|
viper.SetDefault("connection.cert", "")
|
||||||
|
viper.SetDefault("connection.key", "")
|
||||||
|
viper.SetDefault("connection.access_tokens", "")
|
||||||
|
viper.SetDefault("connection.retry_enabled", true)
|
||||||
|
viper.SetDefault("connection.retry_attempts", 10)
|
||||||
|
viper.SetDefault("connection.retry_interval", 5)
|
||||||
|
|
||||||
|
// Cache defaults.
|
||||||
|
viper.SetDefault("cache.enabled", false)
|
||||||
|
viper.SetDefault("cache.maximum_size", "512MiB")
|
||||||
|
viper.SetDefault("cache.expire_time", 24)
|
||||||
|
viper.SetDefault("cache.check_interval", 5)
|
||||||
|
viper.SetDefault("cache.directory", "$HOME/.cache/mumbledj")
|
||||||
|
|
||||||
|
// Volume defaults.
|
||||||
|
viper.SetDefault("volume.default", 0.2)
|
||||||
|
viper.SetDefault("volume.lowest", 0.01)
|
||||||
|
viper.SetDefault("volume.highest", 0.8)
|
||||||
|
|
||||||
|
// Admins defaults.
|
||||||
|
viper.SetDefault("admins.enabled", true)
|
||||||
|
viper.SetDefault("admins.names", []string{"SuperUser"})
|
||||||
|
|
||||||
|
// Command defaults.
|
||||||
|
viper.SetDefault("commands.add.aliases", []string{"add", "a"})
|
||||||
|
viper.SetDefault("commands.add.is_admin", false)
|
||||||
|
viper.SetDefault("commands.add.description", "Adds a track or playlist from a media site to the queue.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.addnext.aliases", []string{"addnext", "an"})
|
||||||
|
viper.SetDefault("commands.addnext.is_admin", true)
|
||||||
|
viper.SetDefault("commands.addnext.description", "Adds a track or playlist from a media site as the next item in the queue.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.cachesize.aliases", []string{"cachesize", "cs"})
|
||||||
|
viper.SetDefault("commands.cachesize.is_admin", true)
|
||||||
|
viper.SetDefault("commands.cachesize.description", "Outputs the file size of the cache in MiB if caching is enabled.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.currenttrack.aliases", []string{"currenttrack", "currentsong", "current"})
|
||||||
|
viper.SetDefault("commands.currenttrack.is_admin", false)
|
||||||
|
viper.SetDefault("commands.currenttrack.description", "Outputs information about the current track in the queue if one exists.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.forceskip.aliases", []string{"forceskip", "fs"})
|
||||||
|
viper.SetDefault("commands.forceskip.is_admin", true)
|
||||||
|
viper.SetDefault("commands.forceskip.description", "Immediately skips the current track.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.forceskipplaylist.aliases", []string{"forceskipplaylist", "fsp"})
|
||||||
|
viper.SetDefault("commands.forceskipplaylist.is_admin", true)
|
||||||
|
viper.SetDefault("commands.forceskipplaylist.description", "Immediately skips the current playlist.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.help.aliases", []string{"help", "h"})
|
||||||
|
viper.SetDefault("commands.help.is_admin", false)
|
||||||
|
viper.SetDefault("commands.help.description", "Outputs this list of commands.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.joinme.aliases", []string{"joinme", "join"})
|
||||||
|
viper.SetDefault("commands.joinme.is_admin", true)
|
||||||
|
viper.SetDefault("commands.joinme.description", "Moves MumbleDJ into your current channel if not playing audio to someone else.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.kill.aliases", []string{"kill", "k"})
|
||||||
|
viper.SetDefault("commands.kill.is_admin", true)
|
||||||
|
viper.SetDefault("commands.kill.description", "Stops the bot and cleans its cache directory.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.listtracks.aliases", []string{"listtracks", "listsongs", "list", "l"})
|
||||||
|
viper.SetDefault("commands.listtracks.is_admin", false)
|
||||||
|
viper.SetDefault("commands.listtracks.description", "Outputs a list of the tracks currently in the queue.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.move.aliases", []string{"move", "m"})
|
||||||
|
viper.SetDefault("commands.move.is_admin", true)
|
||||||
|
viper.SetDefault("commands.move.description", "Moves the bot into the Mumble channel provided via argument.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.nexttrack.aliases", []string{"nexttrack", "nextsong", "next"})
|
||||||
|
viper.SetDefault("commands.nexttrack.is_admin", false)
|
||||||
|
viper.SetDefault("commands.nexttrack.description", "Outputs information about the next track in the queue if one exists.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.numcached.aliases", []string{"numcached", "nc"})
|
||||||
|
viper.SetDefault("commands.numcached.is_admin", true)
|
||||||
|
viper.SetDefault("commands.numcached.description", "Outputs the number of tracks cached on disk if caching is enabled.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.numtracks.aliases", []string{"numtracks", "numsongs", "nt"})
|
||||||
|
viper.SetDefault("commands.numtracks.is_admin", false)
|
||||||
|
viper.SetDefault("commands.numtracks.description", "Outputs the number of tracks currently in the queue.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.pause.aliases", []string{"pause"})
|
||||||
|
viper.SetDefault("commands.pause.is_admin", false)
|
||||||
|
viper.SetDefault("commands.pause.description", "Pauses audio playback.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.reload.aliases", []string{"reload", "r"})
|
||||||
|
viper.SetDefault("commands.reload.is_admin", true)
|
||||||
|
viper.SetDefault("commands.reload.description", "Reloads the configuration file.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.reset.aliases", []string{"reset", "re"})
|
||||||
|
viper.SetDefault("commands.reset.is_admin", true)
|
||||||
|
viper.SetDefault("commands.reset.description", "Resets the queue by removing all queue items.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.resume.aliases", []string{"resume"})
|
||||||
|
viper.SetDefault("commands.resume.is_admin", false)
|
||||||
|
viper.SetDefault("commands.resume.description", "Resumes audio playback.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.setcomment.aliases", []string{"setcomment", "comment", "sc"})
|
||||||
|
viper.SetDefault("commands.setcomment.is_admin", true)
|
||||||
|
viper.SetDefault("commands.setcomment.description", "Sets the comment displayed next to MumbleDJ's username in Mumble.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.shuffle.aliases", []string{"shuffle", "shuf", "sh"})
|
||||||
|
viper.SetDefault("commands.shuffle.is_admin", true)
|
||||||
|
viper.SetDefault("commands.shuffle.description", "Randomizes the tracks currently in the queue.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.skip.aliases", []string{"skip", "s"})
|
||||||
|
viper.SetDefault("commands.skip.is_admin", false)
|
||||||
|
viper.SetDefault("commands.skip.description", "Places a vote to skip the current track.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.skipplaylist.aliases", []string{"skipplaylist", "sp"})
|
||||||
|
viper.SetDefault("commands.skipplaylist.is_admin", false)
|
||||||
|
viper.SetDefault("commands.skipplaylist.description", "Places a vote to skip the current playlist.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.toggleshuffle.aliases", []string{"toggleshuffle", "toggleshuf", "togshuf", "tsh"})
|
||||||
|
viper.SetDefault("commands.toggleshuffle.is_admin", true)
|
||||||
|
viper.SetDefault("commands.toggleshuffle.description", "Toggles automatic track shuffling on/off.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.version.aliases", []string{"version"})
|
||||||
|
viper.SetDefault("commands.version.is_admin", false)
|
||||||
|
viper.SetDefault("commands.version.description", "Outputs the current version of MumbleDJ.")
|
||||||
|
|
||||||
|
viper.SetDefault("commands.volume.aliases", []string{"volume", "vol", "v"})
|
||||||
|
viper.SetDefault("commands.volume.is_admin", false)
|
||||||
|
viper.SetDefault("commands.volume.description", "Changes the volume if an argument is provided, outputs the current volume otherwise.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadConfigFile reads in the config file and updates the configuration accordingly.
|
||||||
|
func ReadConfigFile() error {
|
||||||
|
logrus.Infoln("Reading config...")
|
||||||
|
return viper.ReadInConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckForDuplicateAliases validates that all commands have unique aliases.
|
||||||
|
func CheckForDuplicateAliases() error {
|
||||||
|
var aliases []string
|
||||||
|
|
||||||
|
logrus.Infoln("Checking for duplicate aliases...")
|
||||||
|
|
||||||
|
// It would be preferred to use viper.Sub("aliases") here, but there are some
|
||||||
|
// nil pointer dereferencing issues.
|
||||||
|
for _, setting := range viper.AllKeys() {
|
||||||
|
if strings.HasSuffix(setting, "aliases") {
|
||||||
|
aliases = append(aliases, viper.GetStringSlice(setting)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the strings to allow us to fail faster in case there is a duplicate.
|
||||||
|
sort.Strings(aliases)
|
||||||
|
|
||||||
|
for i := 0; i < len(aliases)-1; i++ {
|
||||||
|
if aliases[i] == aliases[i+1] {
|
||||||
|
return fmt.Errorf("Duplicate alias found: %s", aliases[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
56
bot/config_test.go
Normal file
56
bot/config_test.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* bot/config_test.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConfigTestSuite) SetupSuite() {
|
||||||
|
DJ = NewMumbleDJ()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConfigTestSuite) SetupTest() {
|
||||||
|
viper.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConfigTestSuite) TestCheckForDuplicateAliasesWhenNoDuplicatesExist() {
|
||||||
|
viper.Set("commands.add.aliases", []string{"add", "a"})
|
||||||
|
viper.Set("commands.addnext.aliases", []string{"addnext", "an"})
|
||||||
|
viper.Set("commands.skip.aliases", []string{"skip", "s"})
|
||||||
|
viper.Set("commands.skipplaylist.aliases", []string{"skipplaylist", "sp"})
|
||||||
|
|
||||||
|
err := CheckForDuplicateAliases()
|
||||||
|
|
||||||
|
suite.Nil(err, "No error should be returned as there are no duplicate aliases.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConfigTestSuite) TestCheckForDuplicateAliasesWhenDuplicatesExist() {
|
||||||
|
viper.Set("commands.add.aliases", []string{"add", "a"})
|
||||||
|
viper.Set("commands.addnext.aliases", []string{"addnext", "an"})
|
||||||
|
viper.Set("commands.skip.aliases", []string{"skip", "s"})
|
||||||
|
viper.Set("commands.skipplaylist.aliases", []string{"skipplaylist", "sp"})
|
||||||
|
viper.Set("commands.version.aliases", []string{"version", "v"})
|
||||||
|
viper.Set("commands.volume.aliases", []string{"volume", "vol", "v"})
|
||||||
|
|
||||||
|
err := CheckForDuplicateAliases()
|
||||||
|
|
||||||
|
suite.NotNil(err, "An error should be returned as there are duplicate aliases.")
|
||||||
|
suite.Contains(err.Error(), "v", "The error message should contain the duplicate alias.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(ConfigTestSuite))
|
||||||
|
}
|
281
bot/mumbledj.go
Normal file
281
bot/mumbledj.go
Normal file
|
@ -0,0 +1,281 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* bot/mumbledj.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/layeh/gumble/gumbleffmpeg"
|
||||||
|
"github.com/layeh/gumble/gumbleutil"
|
||||||
|
"github.com/matthieugrieger/mumbledj/interfaces"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MumbleDJ is a struct that keeps track of all aspects of the bot's state.
|
||||||
|
type MumbleDJ struct {
|
||||||
|
AvailableServices []interfaces.Service
|
||||||
|
Client *gumble.Client
|
||||||
|
GumbleConfig *gumble.Config
|
||||||
|
TLSConfig *tls.Config
|
||||||
|
AudioStream *gumbleffmpeg.Stream
|
||||||
|
Queue interfaces.Queue
|
||||||
|
Cache *Cache
|
||||||
|
Skips interfaces.SkipTracker
|
||||||
|
Commands []interfaces.Command
|
||||||
|
Version string
|
||||||
|
Volume float32
|
||||||
|
YouTubeDL *YouTubeDL
|
||||||
|
KeepAlive chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// DJ is a struct that keeps track of all aspects of MumbleDJ's environment.
|
||||||
|
var DJ *MumbleDJ
|
||||||
|
|
||||||
|
// NewMumbleDJ initializes and returns a MumbleDJ type.
|
||||||
|
func NewMumbleDJ() *MumbleDJ {
|
||||||
|
SetDefaultConfig()
|
||||||
|
|
||||||
|
return &MumbleDJ{
|
||||||
|
AvailableServices: make([]interfaces.Service, 0),
|
||||||
|
TLSConfig: new(tls.Config),
|
||||||
|
Queue: NewQueue(),
|
||||||
|
Cache: NewCache(),
|
||||||
|
Skips: NewSkipTracker(),
|
||||||
|
Commands: make([]interfaces.Command, 0),
|
||||||
|
YouTubeDL: new(YouTubeDL),
|
||||||
|
KeepAlive: make(chan bool),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnConnect event. First moves MumbleDJ into the default channel if one exists.
|
||||||
|
// The configuration is loaded and the audio stream is initialized.
|
||||||
|
func (dj *MumbleDJ) OnConnect(e *gumble.ConnectEvent) {
|
||||||
|
dj.AudioStream = nil
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"volume": fmt.Sprintf("%.2f", viper.GetFloat64("volume.default")),
|
||||||
|
}).Infoln("Setting default volume...")
|
||||||
|
dj.Volume = float32(viper.GetFloat64("volume.default"))
|
||||||
|
|
||||||
|
if viper.GetBool("cache.enabled") {
|
||||||
|
logrus.Infoln("Caching enabled.")
|
||||||
|
dj.Cache.UpdateStatistics()
|
||||||
|
go dj.Cache.CleanPeriodically()
|
||||||
|
} else {
|
||||||
|
logrus.Infoln("Caching disabled.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnDisconnect event. Terminates MumbleDJ process or retries connection if
|
||||||
|
// automatic connection retries are enabled.
|
||||||
|
func (dj *MumbleDJ) OnDisconnect(e *gumble.DisconnectEvent) {
|
||||||
|
if viper.GetBool("connection.retry_enabled") &&
|
||||||
|
(e.Type == gumble.DisconnectError || e.Type == gumble.DisconnectKicked) {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"interval_secs": fmt.Sprintf("%d", viper.GetInt("connection.retry_interval")),
|
||||||
|
"attempts": fmt.Sprintf("%d", viper.GetInt("connection.retry_attempts")),
|
||||||
|
}).Warnln("Disconnected from server. Retrying connection...")
|
||||||
|
|
||||||
|
success := false
|
||||||
|
for retries := 0; retries < viper.GetInt("connection.retry_attempts"); retries++ {
|
||||||
|
logrus.Infoln("Retrying connection...")
|
||||||
|
if client, err := gumble.DialWithDialer(new(net.Dialer), viper.GetString("connection.address")+":"+viper.GetString("connection.port"), dj.GumbleConfig, dj.TLSConfig); err == nil {
|
||||||
|
dj.Client = client
|
||||||
|
logrus.Infoln("Successfully reconnected to the server!")
|
||||||
|
success = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(time.Duration(viper.GetInt("connection.retry_interval")) * time.Second)
|
||||||
|
}
|
||||||
|
if !success {
|
||||||
|
dj.KeepAlive <- true
|
||||||
|
logrus.Fatalln("Could not reconnect to server. Exiting...")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dj.KeepAlive <- true
|
||||||
|
logrus.Fatalln("Disconnected from server. No reconnect attempts will be made.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnTextMessage event. Checks for command prefix and passes it to the Commander
|
||||||
|
// if it exists. Ignores the incoming message otherwise.
|
||||||
|
func (dj *MumbleDJ) OnTextMessage(e *gumble.TextMessageEvent) {
|
||||||
|
plainMessage := gumbleutil.PlainText(&e.TextMessage)
|
||||||
|
if len(plainMessage) != 0 {
|
||||||
|
if plainMessage[0] == viper.GetString("commands.prefix")[0] &&
|
||||||
|
plainMessage != viper.GetString("commands.prefix") {
|
||||||
|
go func() {
|
||||||
|
message, isPrivateMessage, err := dj.FindAndExecuteCommand(e.Sender, plainMessage[1:])
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"user": e.Sender.Name,
|
||||||
|
"message": err.Error(),
|
||||||
|
}).Warnln("Sending an error message...")
|
||||||
|
dj.SendPrivateMessage(e.Sender, fmt.Sprintf("<b>Error:</b> %s.", err.Error()))
|
||||||
|
} else {
|
||||||
|
if isPrivateMessage {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"user": e.Sender.Name,
|
||||||
|
"message": message,
|
||||||
|
}).Infoln("Sending a private message...")
|
||||||
|
dj.SendPrivateMessage(e.Sender, message)
|
||||||
|
} else {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"channel": dj.Client.Self.Channel.Name,
|
||||||
|
"message": message,
|
||||||
|
}).Infoln("Sending a message to channel...")
|
||||||
|
dj.Client.Self.Channel.Send(message, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnUserChange event. Checks UserChange type and adjusts skip trackers to
|
||||||
|
// reflect the current status of the users on the server.
|
||||||
|
func (dj *MumbleDJ) OnUserChange(e *gumble.UserChangeEvent) {
|
||||||
|
if e.Type.Has(gumble.UserChangeDisconnected) || e.Type.Has(gumble.UserChangeChannel) {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"user": e.User.Name,
|
||||||
|
}).Infoln("A user has disconnected or changed channels, updating skip trackers...")
|
||||||
|
dj.Skips.RemoveTrackSkip(e.User)
|
||||||
|
dj.Skips.RemovePlaylistSkip(e.User)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendPrivateMessage sends a private message to the specified user. This method
|
||||||
|
// verifies that the targeted user is still present in the server before attempting
|
||||||
|
// to send the message.
|
||||||
|
func (dj *MumbleDJ) SendPrivateMessage(user *gumble.User, message string) {
|
||||||
|
dj.Client.Do(func() {
|
||||||
|
if targetUser := dj.Client.Self.Channel.Users.Find(user.Name); targetUser != nil {
|
||||||
|
targetUser.Send(message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdmin checks whether a particular Mumble user is a MumbleDJ admin.
|
||||||
|
// Returns true if the user is an admin, and false otherwise.
|
||||||
|
func (dj *MumbleDJ) IsAdmin(user *gumble.User) bool {
|
||||||
|
for _, admin := range viper.GetStringSlice("admins.names") {
|
||||||
|
if user.Name == admin {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect starts the process for connecting to a Mumble server.
|
||||||
|
func (dj *MumbleDJ) Connect() error {
|
||||||
|
// Perform startup checks before connecting.
|
||||||
|
logrus.Infoln("Performing startup checks...")
|
||||||
|
PerformStartupChecks()
|
||||||
|
|
||||||
|
// Create Gumble config.
|
||||||
|
dj.GumbleConfig = gumble.NewConfig()
|
||||||
|
dj.GumbleConfig.Username = viper.GetString("connection.username")
|
||||||
|
dj.GumbleConfig.Password = viper.GetString("connection.password")
|
||||||
|
dj.GumbleConfig.Tokens = strings.Split(viper.GetString("connection.access_tokens"), ",")
|
||||||
|
|
||||||
|
// Initialize key pair if needed.
|
||||||
|
if viper.GetBool("connection.insecure") {
|
||||||
|
dj.TLSConfig.InsecureSkipVerify = true
|
||||||
|
}
|
||||||
|
if viper.GetString("connection.cert") != "" {
|
||||||
|
if viper.GetString("connection.key") != "" {
|
||||||
|
viper.Set("connection.key", viper.GetString("connection.cert"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if certificate, err := tls.LoadX509KeyPair(viper.GetString("connection.cert"), viper.GetString("connection.key")); err == nil {
|
||||||
|
dj.TLSConfig.Certificates = append(dj.TLSConfig.Certificates, certificate)
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dj.GumbleConfig.Attach(gumbleutil.Listener{
|
||||||
|
Connect: dj.OnConnect,
|
||||||
|
Disconnect: dj.OnDisconnect,
|
||||||
|
TextMessage: dj.OnTextMessage,
|
||||||
|
UserChange: dj.OnUserChange,
|
||||||
|
})
|
||||||
|
dj.GumbleConfig.Attach(gumbleutil.AutoBitrate)
|
||||||
|
|
||||||
|
var connErr error
|
||||||
|
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"address": viper.GetString("connection.address"),
|
||||||
|
"port": viper.GetString("connection.port"),
|
||||||
|
}).Infoln("Attempting connection to server...")
|
||||||
|
if dj.Client, connErr = gumble.DialWithDialer(new(net.Dialer), viper.GetString("connection.address")+":"+viper.GetString("connection.port"), dj.GumbleConfig, dj.TLSConfig); connErr != nil {
|
||||||
|
return connErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindAndExecuteCommand attempts to find a reference to a command in an
|
||||||
|
// incoming message. If found, the command is executed and the resulting
|
||||||
|
// message/error is returned.
|
||||||
|
func (dj *MumbleDJ) FindAndExecuteCommand(user *gumble.User, message string) (string, bool, error) {
|
||||||
|
command, err := dj.findCommand(message)
|
||||||
|
if err != nil {
|
||||||
|
return "", true, errors.New("No command was found in this message")
|
||||||
|
}
|
||||||
|
return dj.executeCommand(user, message, command)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetService loops through the available services and determines if a URL
|
||||||
|
// matches a particular service. If a match is found, the service object is
|
||||||
|
// returned.
|
||||||
|
func (dj *MumbleDJ) GetService(url string) (interfaces.Service, error) {
|
||||||
|
for _, service := range dj.AvailableServices {
|
||||||
|
if service.CheckURL(url) {
|
||||||
|
return service, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("The provided URL does not match an enabled service")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dj *MumbleDJ) findCommand(message string) (interfaces.Command, error) {
|
||||||
|
var possibleCommand string
|
||||||
|
if strings.Contains(message, " ") {
|
||||||
|
possibleCommand = strings.ToLower(message[:strings.Index(message, " ")])
|
||||||
|
} else {
|
||||||
|
possibleCommand = strings.ToLower(message)
|
||||||
|
}
|
||||||
|
for _, command := range dj.Commands {
|
||||||
|
for _, alias := range command.Aliases() {
|
||||||
|
if possibleCommand == alias {
|
||||||
|
return command, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("No command was found in this message")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dj *MumbleDJ) executeCommand(user *gumble.User, message string, command interfaces.Command) (string, bool, error) {
|
||||||
|
canExecute := false
|
||||||
|
if viper.GetBool("permissions.enabled") && command.IsAdminCommand() {
|
||||||
|
canExecute = dj.IsAdmin(user)
|
||||||
|
} else {
|
||||||
|
canExecute = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if canExecute {
|
||||||
|
return command.Execute(user, strings.Split(message, " ")[1:]...)
|
||||||
|
}
|
||||||
|
return "", true, errors.New("You do not have permission to execute this command")
|
||||||
|
}
|
36
bot/playlist.go
Normal file
36
bot/playlist.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* bot/playlist.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bot
|
||||||
|
|
||||||
|
// Playlist stores all metadata related to a playlist of tracks.
|
||||||
|
type Playlist struct {
|
||||||
|
ID string
|
||||||
|
Title string
|
||||||
|
Submitter string
|
||||||
|
Service string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the ID of the playlist.
|
||||||
|
func (p *Playlist) GetID() string {
|
||||||
|
return p.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTitle returns the title of the playlist.
|
||||||
|
func (p *Playlist) GetTitle() string {
|
||||||
|
return p.Title
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSubmitter returns the submitter of the playlist.
|
||||||
|
func (p *Playlist) GetSubmitter() string {
|
||||||
|
return p.Submitter
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetService returns the name of the service from which the playlist was retrieved from.
|
||||||
|
func (p *Playlist) GetService() string {
|
||||||
|
return p.Service
|
||||||
|
}
|
48
bot/playlist_test.go
Normal file
48
bot/playlist_test.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* bot/playlist_test.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PlaylistTestSuite struct {
|
||||||
|
Playlist Playlist
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PlaylistTestSuite) SetupTest() {
|
||||||
|
suite.Playlist = Playlist{
|
||||||
|
ID: "id",
|
||||||
|
Title: "title",
|
||||||
|
Submitter: "submitter",
|
||||||
|
Service: "service",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PlaylistTestSuite) TestGetID() {
|
||||||
|
suite.Equal("id", suite.Playlist.GetID())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PlaylistTestSuite) TestGetTitle() {
|
||||||
|
suite.Equal("title", suite.Playlist.GetTitle())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PlaylistTestSuite) TestGetSubmitter() {
|
||||||
|
suite.Equal("submitter", suite.Playlist.GetSubmitter())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PlaylistTestSuite) TestGetService() {
|
||||||
|
suite.Equal("service", suite.Playlist.GetService())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlaylistTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(PlaylistTestSuite))
|
||||||
|
}
|
363
bot/queue.go
Normal file
363
bot/queue.go
Normal file
|
@ -0,0 +1,363 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* bot/queue.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumbleffmpeg"
|
||||||
|
_ "github.com/layeh/gumble/opus"
|
||||||
|
"github.com/matthieugrieger/mumbledj/interfaces"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Queue holds the audio queue itself along with useful methods for
|
||||||
|
// performing actions on the queue.
|
||||||
|
type Queue struct {
|
||||||
|
Queue []interfaces.Track
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rand.Seed(time.Now().UTC().UnixNano())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewQueue initializes a new queue and returns it.
|
||||||
|
func NewQueue() *Queue {
|
||||||
|
return &Queue{
|
||||||
|
Queue: make([]interfaces.Track, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Length returns the length of the queue.
|
||||||
|
func (q *Queue) Length() int {
|
||||||
|
q.mutex.RLock()
|
||||||
|
length := len(q.Queue)
|
||||||
|
q.mutex.RUnlock()
|
||||||
|
return length
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset removes all tracks from the queue.
|
||||||
|
func (q *Queue) Reset() {
|
||||||
|
q.mutex.Lock()
|
||||||
|
q.Queue = q.Queue[:0]
|
||||||
|
q.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendTrack adds a track to the back of the queue.
|
||||||
|
func (q *Queue) AppendTrack(t interfaces.Track) error {
|
||||||
|
q.mutex.Lock()
|
||||||
|
beforeLen := len(q.Queue)
|
||||||
|
|
||||||
|
// An error should never occur here since maxTrackDuration is restricted to
|
||||||
|
// ints. Any error in the configuration will be caught during yaml load.
|
||||||
|
maxTrackDuration, _ := time.ParseDuration(fmt.Sprintf("%ds",
|
||||||
|
viper.GetInt("queue.max_track_duration")))
|
||||||
|
|
||||||
|
if viper.GetInt("queue.max_track_duration") == 0 ||
|
||||||
|
t.GetDuration() <= maxTrackDuration {
|
||||||
|
q.Queue = append(q.Queue, t)
|
||||||
|
} else {
|
||||||
|
q.mutex.Unlock()
|
||||||
|
return errors.New("The track is too long to add to the queue")
|
||||||
|
}
|
||||||
|
if len(q.Queue) == beforeLen+1 {
|
||||||
|
q.mutex.Unlock()
|
||||||
|
q.playIfNeeded()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
q.mutex.Unlock()
|
||||||
|
return errors.New("Could not add track to queue")
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertTrack inserts track `t` at position `i` in the queue.
|
||||||
|
func (q *Queue) InsertTrack(i int, t interfaces.Track) error {
|
||||||
|
q.mutex.Lock()
|
||||||
|
beforeLen := len(q.Queue)
|
||||||
|
|
||||||
|
// An error should never occur here since maxTrackDuration is restricted to
|
||||||
|
// ints. Any error in the configuration will be caught during yaml load.
|
||||||
|
maxTrackDuration, _ := time.ParseDuration(fmt.Sprintf("%ds",
|
||||||
|
viper.GetInt("queue.max_track_duration")))
|
||||||
|
|
||||||
|
if viper.GetInt("queue.max_track_duration") == 0 ||
|
||||||
|
t.GetDuration() <= maxTrackDuration {
|
||||||
|
q.Queue = append(q.Queue, Track{})
|
||||||
|
copy(q.Queue[i+1:], q.Queue[i:])
|
||||||
|
q.Queue[i] = t
|
||||||
|
} else {
|
||||||
|
q.mutex.Unlock()
|
||||||
|
return errors.New("The track is too long to add to the queue")
|
||||||
|
}
|
||||||
|
if len(q.Queue) == beforeLen+1 {
|
||||||
|
q.mutex.Unlock()
|
||||||
|
q.playIfNeeded()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
q.mutex.Unlock()
|
||||||
|
return errors.New("Could not add track to queue")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentTrack returns the current Track.
|
||||||
|
func (q *Queue) CurrentTrack() (interfaces.Track, error) {
|
||||||
|
q.mutex.RLock()
|
||||||
|
if len(q.Queue) != 0 {
|
||||||
|
current := q.Queue[0]
|
||||||
|
q.mutex.RUnlock()
|
||||||
|
return current, nil
|
||||||
|
}
|
||||||
|
q.mutex.RUnlock()
|
||||||
|
return nil, errors.New("There are no tracks currently in the queue")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTrack takes an `index` argument to determine which track to return.
|
||||||
|
// If the track in position `index` exists, it is returned. Otherwise,
|
||||||
|
// nil is returned.
|
||||||
|
func (q *Queue) GetTrack(index int) interfaces.Track {
|
||||||
|
q.mutex.RLock()
|
||||||
|
if index >= len(q.Queue) {
|
||||||
|
q.mutex.RUnlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
track := q.Queue[index]
|
||||||
|
q.mutex.RUnlock()
|
||||||
|
return track
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeekNextTrack peeks at the next track and returns it.
|
||||||
|
func (q *Queue) PeekNextTrack() (interfaces.Track, error) {
|
||||||
|
q.mutex.RLock()
|
||||||
|
if len(q.Queue) > 1 {
|
||||||
|
if viper.GetBool("queue.automatic_shuffle_on") {
|
||||||
|
q.RandomNextTrack(false)
|
||||||
|
}
|
||||||
|
next := q.Queue[1]
|
||||||
|
q.mutex.RUnlock()
|
||||||
|
return next, nil
|
||||||
|
}
|
||||||
|
q.mutex.RUnlock()
|
||||||
|
return nil, errors.New("There is no track coming up next")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traverse is a traversal function for Queue. Allows a visit function to
|
||||||
|
// be passed in which performs the specified action on each queue item.
|
||||||
|
func (q *Queue) Traverse(visit func(i int, t interfaces.Track)) {
|
||||||
|
q.mutex.RLock()
|
||||||
|
if len(q.Queue) > 0 {
|
||||||
|
for queueIndex, queueTrack := range q.Queue {
|
||||||
|
visit(queueIndex, queueTrack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
q.mutex.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShuffleTracks shuffles the queue using an inside-out algorithm.
|
||||||
|
func (q *Queue) ShuffleTracks() {
|
||||||
|
q.mutex.Lock()
|
||||||
|
// Skip the first track, as it is likely playing.
|
||||||
|
for i := range q.Queue[1:] {
|
||||||
|
j := rand.Intn(i + 1)
|
||||||
|
q.Queue[i+1], q.Queue[j+1] = q.Queue[j+1], q.Queue[i+1]
|
||||||
|
}
|
||||||
|
q.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RandomNextTrack sets a random track as the next track to be played.
|
||||||
|
func (q *Queue) RandomNextTrack(queueWasEmpty bool) {
|
||||||
|
q.mutex.Lock()
|
||||||
|
if len(q.Queue) > 1 {
|
||||||
|
nextTrackIndex := 1
|
||||||
|
if queueWasEmpty {
|
||||||
|
nextTrackIndex = 0
|
||||||
|
}
|
||||||
|
swapIndex := nextTrackIndex + rand.Intn(len(q.Queue)-1)
|
||||||
|
q.Queue[nextTrackIndex], q.Queue[swapIndex] = q.Queue[swapIndex], q.Queue[nextTrackIndex]
|
||||||
|
}
|
||||||
|
q.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip performs the necessary actions that take place when a track is skipped
|
||||||
|
// via a command.
|
||||||
|
func (q *Queue) Skip() {
|
||||||
|
// Stop audio stream if one exists.
|
||||||
|
if DJ.AudioStream != nil {
|
||||||
|
q.StopCurrent()
|
||||||
|
DJ.AudioStream = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all track skips.
|
||||||
|
DJ.Skips.ResetTrackSkips()
|
||||||
|
|
||||||
|
q.mutex.Lock()
|
||||||
|
// If caching is disabled, delete the track from disk.
|
||||||
|
if !viper.GetBool("cache.enabled") {
|
||||||
|
DJ.YouTubeDL.Delete(q.Queue[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// If automatic track shuffling is enabled, assign a random track in the queue to be the next track.
|
||||||
|
if viper.GetBool("queue.automatic_shuffle_on") {
|
||||||
|
q.mutex.Unlock()
|
||||||
|
q.RandomNextTrack(false)
|
||||||
|
q.mutex.Lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all playlist skips if this is the last track of the playlist still in the queue.
|
||||||
|
if playlist := q.Queue[0].GetPlaylist(); playlist != nil {
|
||||||
|
id := playlist.GetID()
|
||||||
|
playlistIsFinished := true
|
||||||
|
|
||||||
|
q.mutex.Unlock()
|
||||||
|
q.Traverse(func(i int, t interfaces.Track) {
|
||||||
|
if i != 0 && t.GetPlaylist() != nil {
|
||||||
|
if t.GetPlaylist().GetID() == id {
|
||||||
|
playlistIsFinished = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
q.mutex.Lock()
|
||||||
|
|
||||||
|
if playlistIsFinished {
|
||||||
|
DJ.Skips.ResetPlaylistSkips()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip the track.
|
||||||
|
length := len(q.Queue)
|
||||||
|
if length > 1 {
|
||||||
|
q.Queue = q.Queue[1:]
|
||||||
|
} else {
|
||||||
|
q.Queue = make([]interfaces.Track, 0)
|
||||||
|
}
|
||||||
|
q.mutex.Unlock()
|
||||||
|
|
||||||
|
if err := q.playIfNeeded(); err != nil {
|
||||||
|
q.Skip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SkipPlaylist performs the necessary actions that take place when a playlist
|
||||||
|
// is skipped via a command.
|
||||||
|
func (q *Queue) SkipPlaylist() {
|
||||||
|
q.mutex.Lock()
|
||||||
|
if playlist := q.Queue[0].GetPlaylist(); playlist != nil {
|
||||||
|
currentPlaylistID := playlist.GetID()
|
||||||
|
|
||||||
|
// We must loop backwards to prevent missing any elements after deletion.
|
||||||
|
// NOTE: We do not remove the first track of the playlist quite yet as that
|
||||||
|
// is removed properly with the following Skip() call.
|
||||||
|
for i := len(q.Queue) - 1; i >= 1; i-- {
|
||||||
|
if otherTrackPlaylist := q.Queue[i].GetPlaylist(); otherTrackPlaylist != nil {
|
||||||
|
if otherTrackPlaylist.GetID() == currentPlaylistID {
|
||||||
|
q.Queue = append(q.Queue[:i], q.Queue[i+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
q.mutex.Unlock()
|
||||||
|
q.StopCurrent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlayCurrent creates a new audio stream and begins playing the current track.
|
||||||
|
func (q *Queue) PlayCurrent() error {
|
||||||
|
currentTrack := q.GetTrack(0)
|
||||||
|
filepath := os.ExpandEnv(viper.GetString("cache.directory") + "/" + currentTrack.GetFilename())
|
||||||
|
if _, err := os.Stat(filepath); os.IsNotExist(err) {
|
||||||
|
if err := DJ.YouTubeDL.Download(q.GetTrack(0)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
source := gumbleffmpeg.SourceFile(filepath)
|
||||||
|
DJ.AudioStream = gumbleffmpeg.New(DJ.Client, source)
|
||||||
|
DJ.AudioStream.Offset = currentTrack.GetPlaybackOffset()
|
||||||
|
DJ.AudioStream.Volume = DJ.Volume
|
||||||
|
|
||||||
|
if viper.GetString("defaults.player_command") == "avconv" {
|
||||||
|
DJ.AudioStream.Command = "avconv"
|
||||||
|
}
|
||||||
|
|
||||||
|
if viper.GetBool("queue.announce_new_tracks") {
|
||||||
|
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, currentTrack.GetThumbnailURL(), currentTrack.GetURL(),
|
||||||
|
currentTrack.GetTitle(), currentTrack.GetDuration().String(), currentTrack.GetSubmitter())
|
||||||
|
if currentTrack.GetPlaylist() != nil {
|
||||||
|
message = fmt.Sprintf(message+`<tr><td align="center">From playlist "%s"</td></tr>`, currentTrack.GetPlaylist().GetTitle())
|
||||||
|
}
|
||||||
|
message += `</table>`
|
||||||
|
DJ.Client.Self.Channel.Send(message, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
DJ.AudioStream.Play()
|
||||||
|
go func() {
|
||||||
|
DJ.AudioStream.Wait()
|
||||||
|
q.Skip()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PauseCurrent pauses the current audio stream if it exists and is not already paused.
|
||||||
|
func (q *Queue) PauseCurrent() error {
|
||||||
|
if DJ.AudioStream == nil {
|
||||||
|
return errors.New("There is no track to pause")
|
||||||
|
}
|
||||||
|
if DJ.AudioStream.State() == gumbleffmpeg.StatePaused {
|
||||||
|
return errors.New("The track is already paused")
|
||||||
|
}
|
||||||
|
DJ.AudioStream.Pause()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResumeCurrent resumes playback of the current audio stream if it exists and is paused.
|
||||||
|
func (q *Queue) ResumeCurrent() error {
|
||||||
|
if DJ.AudioStream == nil {
|
||||||
|
return errors.New("There is no track to resume")
|
||||||
|
}
|
||||||
|
if DJ.AudioStream.State() == gumbleffmpeg.StatePlaying {
|
||||||
|
return errors.New("The track is already playing")
|
||||||
|
}
|
||||||
|
DJ.AudioStream.Play()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopCurrent stops the playback of the current audio stream if it exists.
|
||||||
|
func (q *Queue) StopCurrent() error {
|
||||||
|
if DJ.AudioStream == nil {
|
||||||
|
return errors.New("The audio stream is nil")
|
||||||
|
}
|
||||||
|
DJ.AudioStream.Stop()
|
||||||
|
DJ.AudioStream = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queue) playIfNeeded() error {
|
||||||
|
if DJ.AudioStream == nil && q.Length() > 0 {
|
||||||
|
if err := DJ.YouTubeDL.Download(q.GetTrack(0)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := q.PlayCurrent(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
276
bot/queue_test.go
Normal file
276
bot/queue_test.go
Normal file
|
@ -0,0 +1,276 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* bot/queue_test.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumbleffmpeg"
|
||||||
|
"github.com/matthieugrieger/mumbledj/interfaces"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type QueueTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
FirstTrack *Track
|
||||||
|
SecondTrack *Track
|
||||||
|
ThirdTrack *Track
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QueueTestSuite) SetupSuite() {
|
||||||
|
DJ = NewMumbleDJ()
|
||||||
|
|
||||||
|
// Trick the tests into thinking audio is already playing to avoid
|
||||||
|
// attempting to play tracks that don't exist.
|
||||||
|
DJ.AudioStream = new(gumbleffmpeg.Stream)
|
||||||
|
|
||||||
|
viper.Set("queue.automatic_shuffle_on", false)
|
||||||
|
|
||||||
|
suite.FirstTrack = &Track{ID: "first"}
|
||||||
|
suite.SecondTrack = &Track{ID: "second"}
|
||||||
|
suite.ThirdTrack = &Track{ID: "third"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QueueTestSuite) SetupTest() {
|
||||||
|
DJ.Queue = NewQueue()
|
||||||
|
viper.Set("queue.max_track_duration", 0)
|
||||||
|
|
||||||
|
// Override the initialized seed for consistent test results.
|
||||||
|
rand.Seed(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QueueTestSuite) TestNewQueue() {
|
||||||
|
suite.Zero(DJ.Queue.Length(), "The new queue should be empty.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QueueTestSuite) TestAppendTrackWhenTrackIsValid() {
|
||||||
|
suite.Zero(DJ.Queue.Length(), "The queue should be empty.")
|
||||||
|
err := DJ.Queue.AppendTrack(suite.FirstTrack)
|
||||||
|
|
||||||
|
suite.Equal(1, DJ.Queue.Length(), "There should now be one track in the queue.")
|
||||||
|
suite.Nil(err, "No error should be returned.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QueueTestSuite) TestAppendTrackWhenTrackIsTooLong() {
|
||||||
|
// Set max track duration to 5 seconds
|
||||||
|
viper.Set("queue.max_track_duration", 5)
|
||||||
|
|
||||||
|
// Create duration longer than 5 seconds
|
||||||
|
duration, _ := time.ParseDuration("6s")
|
||||||
|
|
||||||
|
longTrack := &Track{}
|
||||||
|
|
||||||
|
longTrack.Duration = duration
|
||||||
|
|
||||||
|
suite.Zero(DJ.Queue.Length(), "The queue should be empty.")
|
||||||
|
err := DJ.Queue.AppendTrack(longTrack)
|
||||||
|
|
||||||
|
suite.Zero(DJ.Queue.Length(), "The queue should still be empty.")
|
||||||
|
suite.NotNil(err, "An error should be returned due to the track being too long.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QueueTestSuite) TestCurrentTrackWhenOneExists() {
|
||||||
|
DJ.Queue.AppendTrack(suite.FirstTrack)
|
||||||
|
|
||||||
|
track, err := DJ.Queue.CurrentTrack()
|
||||||
|
|
||||||
|
suite.NotNil(track, "The returned track should be non-nil.")
|
||||||
|
suite.Equal("first", track.GetID(), "The returned track should be the one just added to the queue.")
|
||||||
|
suite.Nil(err, "No error should be returned.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QueueTestSuite) TestCurrentTrackWhenOneDoesNotExist() {
|
||||||
|
track, err := DJ.Queue.CurrentTrack()
|
||||||
|
|
||||||
|
suite.Nil(track, "The returned track should be nil.")
|
||||||
|
suite.NotNil(err, "An error should be returned because there are no tracks in the queue.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QueueTestSuite) TestPeekNextTrackWhenOneExists() {
|
||||||
|
DJ.Queue.AppendTrack(suite.FirstTrack)
|
||||||
|
DJ.Queue.AppendTrack(suite.SecondTrack)
|
||||||
|
|
||||||
|
track, err := DJ.Queue.PeekNextTrack()
|
||||||
|
|
||||||
|
suite.NotNil(track, "The returned track should be non-nil.")
|
||||||
|
suite.Equal("second", track.GetID(), "The returned track should be the second one added to the queue.")
|
||||||
|
suite.Nil(err, "No error should be returned.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QueueTestSuite) TestPeekNextTrackWhenOneDoesNotExist() {
|
||||||
|
track, err := DJ.Queue.PeekNextTrack()
|
||||||
|
|
||||||
|
suite.Nil(track, "The returned track should be nil.")
|
||||||
|
suite.NotNil(err, "An error should be returned because there are no tracks in the queue.")
|
||||||
|
|
||||||
|
DJ.Queue.AppendTrack(suite.FirstTrack)
|
||||||
|
|
||||||
|
track, err = DJ.Queue.PeekNextTrack()
|
||||||
|
|
||||||
|
suite.Nil(track, "The returned track should be nil.")
|
||||||
|
suite.NotNil(err, "An error should be returned because there is only one track in the queue.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QueueTestSuite) TestTraverseWhenNoTracksExist() {
|
||||||
|
trackString := ""
|
||||||
|
|
||||||
|
DJ.Queue.Traverse(func(i int, t interfaces.Track) {
|
||||||
|
trackString += t.GetID() + ", "
|
||||||
|
})
|
||||||
|
|
||||||
|
suite.Equal("", trackString, "No tracks should be traversed as there are none in the queue.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QueueTestSuite) TestTraverseWhenTracksExist() {
|
||||||
|
trackString := ""
|
||||||
|
DJ.Queue.AppendTrack(suite.FirstTrack)
|
||||||
|
DJ.Queue.AppendTrack(suite.SecondTrack)
|
||||||
|
|
||||||
|
DJ.Queue.Traverse(func(i int, t interfaces.Track) {
|
||||||
|
trackString += t.GetID() + ", "
|
||||||
|
})
|
||||||
|
|
||||||
|
suite.NotEqual("", trackString, "The trackString should not be empty as there were tracks to traverse.")
|
||||||
|
suite.Contains(trackString, "first", "The traverse method should have visited the first track.")
|
||||||
|
suite.Contains(trackString, "second", "The traverse method should have visited the second track.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QueueTestSuite) TestShuffleTracks() {
|
||||||
|
DJ.Queue.AppendTrack(suite.FirstTrack)
|
||||||
|
|
||||||
|
DJ.Queue.ShuffleTracks()
|
||||||
|
|
||||||
|
suite.Equal(suite.FirstTrack, DJ.Queue.GetTrack(0), "Shuffle shouldn't do anything when only one track is in the queue.")
|
||||||
|
|
||||||
|
DJ.Queue.AppendTrack(suite.SecondTrack)
|
||||||
|
|
||||||
|
DJ.Queue.ShuffleTracks()
|
||||||
|
suite.Equal(suite.FirstTrack, DJ.Queue.GetTrack(0), "Shuffle shouldn't do anything when only two tracks are in the queue.")
|
||||||
|
suite.Equal(suite.SecondTrack, DJ.Queue.GetTrack(1), "Shuffle shouldn't do anything when only two tracks are in the queue.")
|
||||||
|
|
||||||
|
DJ.Queue.AppendTrack(suite.ThirdTrack)
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
DJ.Queue.AppendTrack(&Track{ID: fmt.Sprintf("%d", i+4)})
|
||||||
|
}
|
||||||
|
|
||||||
|
originalSecondTrack := DJ.Queue.GetTrack(1)
|
||||||
|
DJ.Queue.ShuffleTracks()
|
||||||
|
suite.NotEqual(originalSecondTrack, DJ.Queue.GetTrack(1), "The shuffled queue should not be the same as the original queue.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QueueTestSuite) TestRandomNextTrackWhenQueueWasEmpty() {
|
||||||
|
DJ.Queue.AppendTrack(suite.FirstTrack)
|
||||||
|
|
||||||
|
DJ.Queue.RandomNextTrack(true)
|
||||||
|
|
||||||
|
suite.Equal(suite.FirstTrack, DJ.Queue.GetTrack(0), "RandomNextTrack shouldn't do anything when there is only one track in the queue.")
|
||||||
|
|
||||||
|
for i := 0; i < 25; i++ {
|
||||||
|
DJ.Queue.AppendTrack(&Track{ID: fmt.Sprintf("%d", i+1)})
|
||||||
|
}
|
||||||
|
|
||||||
|
DJ.Queue.RandomNextTrack(true)
|
||||||
|
|
||||||
|
suite.NotEqual(suite.FirstTrack, DJ.Queue.GetTrack(0), "The first track should no longer be the same.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QueueTestSuite) TestRandomNextTrackWhenQueueWasNotEmpty() {
|
||||||
|
DJ.Queue.AppendTrack(suite.FirstTrack)
|
||||||
|
DJ.Queue.RandomNextTrack(false)
|
||||||
|
|
||||||
|
suite.Equal(suite.FirstTrack, DJ.Queue.GetTrack(0), "RandomNextTrack shouldn't do anything when there is only one track in the queue.")
|
||||||
|
|
||||||
|
DJ.Queue.AppendTrack(suite.SecondTrack)
|
||||||
|
DJ.Queue.RandomNextTrack(false)
|
||||||
|
|
||||||
|
suite.Equal(suite.FirstTrack, DJ.Queue.GetTrack(0), "RandomNextTrack shouldn't do anything when there is only two tracks in the queue and the queue was not previously empty.")
|
||||||
|
suite.Equal(suite.SecondTrack, DJ.Queue.GetTrack(1), "RandomNextTrack shouldn't do anything when there is only two tracks in the queue and the queue was not previously empty.")
|
||||||
|
|
||||||
|
for i := 0; i < 25; i++ {
|
||||||
|
DJ.Queue.AppendTrack(&Track{ID: fmt.Sprintf("%d", i+2)})
|
||||||
|
}
|
||||||
|
|
||||||
|
DJ.Queue.RandomNextTrack(false)
|
||||||
|
|
||||||
|
suite.Equal(suite.FirstTrack, DJ.Queue.GetTrack(0), "Since the queue was not previously empty the first track should not be touched.")
|
||||||
|
suite.NotEqual(suite.SecondTrack, DJ.Queue.GetTrack(1), "The next track should be randomized.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Fix these tests.
|
||||||
|
/*func (suite *QueueTestSuite) TestSkipWhenQueueHasLessThanTwoTracks() {
|
||||||
|
DJ.Queue.AppendTrack(suite.FirstTrack)
|
||||||
|
suite.Equal(1, DJ.Queue.Length(), "There should be one item in the queue.")
|
||||||
|
|
||||||
|
DJ.Queue.Skip()
|
||||||
|
suite.Zero(DJ.Queue.Length(), "There should now be zero items in the queue.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QueueTestSuite) TestSkipWhenQueueHasTwoOrMoreTracks() {
|
||||||
|
DJ.Queue.AppendTrack(suite.FirstTrack)
|
||||||
|
DJ.Queue.AppendTrack(suite.SecondTrack)
|
||||||
|
|
||||||
|
suite.Equal(suite.FirstTrack, DJ.Queue.GetTrack(0), "The track added first should be at the front of the queue.")
|
||||||
|
suite.Equal(2, DJ.Queue.Length(), "There should be two items in the queue.")
|
||||||
|
|
||||||
|
DJ.Queue.Skip()
|
||||||
|
|
||||||
|
suite.Equal(suite.SecondTrack, DJ.Queue.GetTrack(0), "The track added second should be at the front of the queue.")
|
||||||
|
suite.Equal(1, DJ.Queue.Length(), "There should be one item in the queue.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QueueTestSuite) TestSkipPlaylistWhenFirstTrackIsNotPartOfPlaylist() {
|
||||||
|
DJ.Queue.AppendTrack(suite.FirstTrack)
|
||||||
|
DJ.Queue.AppendTrack(suite.SecondTrack)
|
||||||
|
DJ.Queue.AppendTrack(suite.ThirdTrack)
|
||||||
|
|
||||||
|
DJ.Queue.SkipPlaylist()
|
||||||
|
|
||||||
|
suite.Equal(3, DJ.Queue.Length(), "No tracks should be skipped.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QueueTestSuite) TestSkipPlaylistWhenFirstTrackIsPartOfPlaylist() {
|
||||||
|
playlist := &Playlist{ID: "playlist"}
|
||||||
|
track1 := &Track{Playlist: playlist}
|
||||||
|
track2 := &Track{Playlist: playlist}
|
||||||
|
track3 := &Track{}
|
||||||
|
|
||||||
|
DJ.Queue.AppendTrack(track1)
|
||||||
|
DJ.Queue.AppendTrack(track2)
|
||||||
|
DJ.Queue.AppendTrack(track3)
|
||||||
|
|
||||||
|
suite.Equal(3, DJ.Queue.Length(), "There should be three tracks in the queue.")
|
||||||
|
DJ.Queue.SkipPlaylist()
|
||||||
|
suite.Equal(1, DJ.Queue.Length(), "There should be one track remaining in the queue.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *QueueTestSuite) TestSkipPlaylistWhenPlaylistIsShuffled() {
|
||||||
|
playlist := &Playlist{ID: "playlist"}
|
||||||
|
otherPlaylist := &Playlist{ID: "otherplaylist"}
|
||||||
|
track1 := &Track{Playlist: playlist}
|
||||||
|
track2 := &Track{}
|
||||||
|
track3 := &Track{Playlist: otherPlaylist}
|
||||||
|
track4 := &Track{Playlist: playlist}
|
||||||
|
|
||||||
|
DJ.Queue.AppendTrack(track1)
|
||||||
|
DJ.Queue.AppendTrack(track2)
|
||||||
|
DJ.Queue.AppendTrack(track3)
|
||||||
|
DJ.Queue.AppendTrack(track4)
|
||||||
|
|
||||||
|
suite.Equal(4, DJ.Queue.Length(), "There should be four tracks in the queue.")
|
||||||
|
DJ.Queue.SkipPlaylist()
|
||||||
|
suite.Equal(2, DJ.Queue.Length(), "There should be two tracks remaining in the queue.")
|
||||||
|
}*/
|
||||||
|
|
||||||
|
func TestQueueTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(QueueTestSuite))
|
||||||
|
}
|
144
bot/skiptracker.go
Normal file
144
bot/skiptracker.go
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* bot/skiptracker.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SkipTracker keeps track of the list of users who have skipped the current
|
||||||
|
// track or playlist.
|
||||||
|
type SkipTracker struct {
|
||||||
|
TrackSkips []*gumble.User
|
||||||
|
PlaylistSkips []*gumble.User
|
||||||
|
trackMutex sync.RWMutex
|
||||||
|
playlistMutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSkipTracker returns an empty SkipTracker.
|
||||||
|
func NewSkipTracker() *SkipTracker {
|
||||||
|
return &SkipTracker{
|
||||||
|
TrackSkips: make([]*gumble.User, 0),
|
||||||
|
PlaylistSkips: make([]*gumble.User, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTrackSkip adds a skip to the SkipTracker for the current track.
|
||||||
|
func (s *SkipTracker) AddTrackSkip(skipper *gumble.User) error {
|
||||||
|
s.trackMutex.Lock()
|
||||||
|
for _, user := range s.TrackSkips {
|
||||||
|
if user.Name == skipper.Name {
|
||||||
|
s.trackMutex.Unlock()
|
||||||
|
return fmt.Errorf("%s has already voted to skip the track", skipper.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.TrackSkips = append(s.TrackSkips, skipper)
|
||||||
|
s.trackMutex.Unlock()
|
||||||
|
s.evaluateTrackSkips()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddPlaylistSkip adds a skip to the SkipTracker for the current playlist.
|
||||||
|
func (s *SkipTracker) AddPlaylistSkip(skipper *gumble.User) error {
|
||||||
|
s.playlistMutex.Lock()
|
||||||
|
for _, user := range s.PlaylistSkips {
|
||||||
|
if user.Name == skipper.Name {
|
||||||
|
s.playlistMutex.Unlock()
|
||||||
|
return fmt.Errorf("%s has already voted to skip the playlist", skipper.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.PlaylistSkips = append(s.PlaylistSkips, skipper)
|
||||||
|
s.playlistMutex.Unlock()
|
||||||
|
s.evaluatePlaylistSkips()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveTrackSkip removes a skip from the SkipTracker for the current track.
|
||||||
|
func (s *SkipTracker) RemoveTrackSkip(skipper *gumble.User) error {
|
||||||
|
s.trackMutex.Lock()
|
||||||
|
for i, user := range s.TrackSkips {
|
||||||
|
if user.Name == skipper.Name {
|
||||||
|
s.TrackSkips = append(s.TrackSkips[:i], s.TrackSkips[i+1:]...)
|
||||||
|
s.trackMutex.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.trackMutex.Unlock()
|
||||||
|
return fmt.Errorf("%s did not previously vote to skip the track", skipper.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemovePlaylistSkip removes a skip from the SkipTracker for the current playlist.
|
||||||
|
func (s *SkipTracker) RemovePlaylistSkip(skipper *gumble.User) error {
|
||||||
|
s.playlistMutex.Lock()
|
||||||
|
for i, user := range s.PlaylistSkips {
|
||||||
|
if user.Name == skipper.Name {
|
||||||
|
s.PlaylistSkips = append(s.PlaylistSkips[:i], s.PlaylistSkips[i+1:]...)
|
||||||
|
s.playlistMutex.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.playlistMutex.Unlock()
|
||||||
|
return fmt.Errorf("%s did not previously vote to skip the playlist", skipper.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumTrackSkips returns the number of users who have skipped the current track.
|
||||||
|
func (s *SkipTracker) NumTrackSkips() int {
|
||||||
|
s.trackMutex.RLock()
|
||||||
|
length := len(s.TrackSkips)
|
||||||
|
s.trackMutex.RUnlock()
|
||||||
|
return length
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumPlaylistSkips returns the number of users who have skipped the current playlist.
|
||||||
|
func (s *SkipTracker) NumPlaylistSkips() int {
|
||||||
|
s.playlistMutex.RLock()
|
||||||
|
length := len(s.PlaylistSkips)
|
||||||
|
s.playlistMutex.RUnlock()
|
||||||
|
return length
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetTrackSkips resets the skip slice for the current track.
|
||||||
|
func (s *SkipTracker) ResetTrackSkips() {
|
||||||
|
s.trackMutex.Lock()
|
||||||
|
s.TrackSkips = s.TrackSkips[:0]
|
||||||
|
s.trackMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetPlaylistSkips resets the skip slice for the current playlist.
|
||||||
|
func (s *SkipTracker) ResetPlaylistSkips() {
|
||||||
|
s.playlistMutex.Lock()
|
||||||
|
s.PlaylistSkips = s.PlaylistSkips[:0]
|
||||||
|
s.playlistMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SkipTracker) evaluateTrackSkips() {
|
||||||
|
s.trackMutex.RLock()
|
||||||
|
skipRatio := viper.GetFloat64("queue.track_skip_ratio")
|
||||||
|
DJ.Client.Do(func() {
|
||||||
|
if float64(len(s.TrackSkips))/float64(len(DJ.Client.Self.Channel.Users)) >= skipRatio {
|
||||||
|
// Stopping an audio stream triggers a skip.
|
||||||
|
DJ.Queue.StopCurrent()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
s.trackMutex.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SkipTracker) evaluatePlaylistSkips() {
|
||||||
|
s.playlistMutex.RLock()
|
||||||
|
skipRatio := viper.GetFloat64("queue.playlist_skip_ratio")
|
||||||
|
DJ.Client.Do(func() {
|
||||||
|
if float64(len(s.PlaylistSkips))/float64(len(DJ.Client.Self.Channel.Users)) >= skipRatio {
|
||||||
|
DJ.Queue.SkipPlaylist()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
s.playlistMutex.RUnlock()
|
||||||
|
}
|
143
bot/skiptracker_test.go
Normal file
143
bot/skiptracker_test.go
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* bot/skiptracker_test.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SkipTrackerTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
Skips *SkipTracker
|
||||||
|
User1 *gumble.User
|
||||||
|
User2 *gumble.User
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SkipTrackerTestSuite) SetupSuite() {
|
||||||
|
suite.User1 = new(gumble.User)
|
||||||
|
suite.User1.Name = "User1"
|
||||||
|
suite.User2 = new(gumble.User)
|
||||||
|
suite.User2.Name = "User2"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SkipTrackerTestSuite) SetupTest() {
|
||||||
|
suite.Skips = NewSkipTracker()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SkipTrackerTestSuite) TestNewSkipTracker() {
|
||||||
|
suite.Zero(suite.Skips.NumTrackSkips(), "The track skip slice should be empty upon initialization.")
|
||||||
|
suite.Zero(suite.Skips.NumPlaylistSkips(), "The playlist skip slice should be empty upon initialization.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Fix these tests.
|
||||||
|
/*func (suite *SkipTrackerTestSuite) TestAddTrackSkip() {
|
||||||
|
err := suite.Skips.AddTrackSkip(suite.User1)
|
||||||
|
|
||||||
|
suite.Equal(1, suite.Skips.NumTrackSkips(), "There should now be one user in the track skip slice.")
|
||||||
|
suite.Zero(0, suite.Skips.NumPlaylistSkips(), "The playlist skip slice should be unaffected.")
|
||||||
|
suite.Nil(err, "No error should be returned.")
|
||||||
|
|
||||||
|
err = suite.Skips.AddTrackSkip(suite.User2)
|
||||||
|
|
||||||
|
suite.Equal(2, suite.Skips.NumTrackSkips(), "There should now be two users in the track skip slice.")
|
||||||
|
suite.Zero(0, suite.Skips.NumPlaylistSkips(), "The playlist skip slice should be unaffected.")
|
||||||
|
suite.Nil(err, "No error should be returned.")
|
||||||
|
|
||||||
|
err = suite.Skips.AddTrackSkip(suite.User1)
|
||||||
|
|
||||||
|
suite.Equal(2, suite.Skips.NumTrackSkips(), "This is a duplicate skip, so the track skip slice should still only have two users.")
|
||||||
|
suite.Zero(0, suite.Skips.NumPlaylistSkips(), "The playlist skip slice should be unaffected.")
|
||||||
|
suite.NotNil(err, "An error should be returned since this user has already voted to skip the current track.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SkipTrackerTestSuite) TestAddPlaylistSkip() {
|
||||||
|
err := suite.Skips.AddPlaylistSkip(suite.User1)
|
||||||
|
|
||||||
|
suite.Zero(suite.Skips.NumTrackSkips(), "The track skip slice should be unaffected.")
|
||||||
|
suite.Equal(1, suite.Skips.NumPlaylistSkips(), "There should now be one user in the playlist skip slice.")
|
||||||
|
suite.Nil(err, "No error should be returned.")
|
||||||
|
|
||||||
|
err = suite.Skips.AddPlaylistSkip(suite.User2)
|
||||||
|
|
||||||
|
suite.Zero(suite.Skips.NumTrackSkips(), "The track skip slice should be unaffected.")
|
||||||
|
suite.Equal(2, suite.Skips.NumPlaylistSkips(), "There should now be two users in the playlist skip slice.")
|
||||||
|
suite.Nil(err, "No error should be returned.")
|
||||||
|
|
||||||
|
err = suite.Skips.AddPlaylistSkip(suite.User1)
|
||||||
|
|
||||||
|
suite.Zero(suite.Skips.NumTrackSkips(), "The track skip slice should be unaffected.")
|
||||||
|
suite.Equal(2, suite.Skips.NumPlaylistSkips(), "This is a duplicate skip, so the playlist skip slice should still only have two users.")
|
||||||
|
suite.NotNil(err, "An error should be returned since this user has already voted to skip the current playlist.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SkipTrackerTestSuite) TestRemoveTrackSkip() {
|
||||||
|
suite.Skips.AddTrackSkip(suite.User1)
|
||||||
|
err := suite.Skips.RemoveTrackSkip(suite.User2)
|
||||||
|
|
||||||
|
suite.Equal(1, suite.Skips.NumTrackSkips(), "User2 has not skipped the track so the track skip slice should be unaffected.")
|
||||||
|
suite.Zero(suite.Skips.NumPlaylistSkips(), "The playlist skip slice should be unaffected.")
|
||||||
|
suite.NotNil(err, "An error should be returned since User2 has not skipped the track yet.")
|
||||||
|
|
||||||
|
err = suite.Skips.RemoveTrackSkip(suite.User1)
|
||||||
|
|
||||||
|
suite.Zero(suite.Skips.NumTrackSkips(), "User1 skipped the track, so their skip should be removed.")
|
||||||
|
suite.Zero(suite.Skips.NumPlaylistSkips(), "The playlist skip slice should be unaffected.")
|
||||||
|
suite.Nil(err, "No error should be returned.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SkipTrackerTestSuite) TestRemovePlaylistSkip() {
|
||||||
|
suite.Skips.AddPlaylistSkip(suite.User1)
|
||||||
|
err := suite.Skips.RemovePlaylistSkip(suite.User2)
|
||||||
|
|
||||||
|
suite.Zero(suite.Skips.NumTrackSkips(), "The track skip slice should be unaffected.")
|
||||||
|
suite.Equal(1, suite.Skips.NumPlaylistSkips(), "User2 has not skipped the playlist so the playlist skip slice should be unaffected.")
|
||||||
|
suite.NotNil(err, "An error should be returned since User2 has not skipped the playlist yet.")
|
||||||
|
|
||||||
|
err = suite.Skips.RemovePlaylistSkip(suite.User1)
|
||||||
|
|
||||||
|
suite.Zero(suite.Skips.NumTrackSkips(), "The track skip slice should be unaffected.")
|
||||||
|
suite.Zero(suite.Skips.NumPlaylistSkips(), "User1 skipped the playlist, so their skip should be removed.")
|
||||||
|
suite.Nil(err, "No error should be returned.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SkipTrackerTestSuite) TestResetTrackSkips() {
|
||||||
|
suite.Skips.AddTrackSkip(suite.User1)
|
||||||
|
suite.Skips.AddTrackSkip(suite.User2)
|
||||||
|
suite.Skips.AddPlaylistSkip(suite.User1)
|
||||||
|
suite.Skips.AddPlaylistSkip(suite.User2)
|
||||||
|
|
||||||
|
suite.Equal(2, suite.Skips.NumTrackSkips(), "There should be two users in the track skip slice.")
|
||||||
|
suite.Equal(2, suite.Skips.NumPlaylistSkips(), "There should be two users in the playlist skip slice.")
|
||||||
|
|
||||||
|
suite.Skips.ResetTrackSkips()
|
||||||
|
|
||||||
|
suite.Zero(suite.Skips.NumTrackSkips(), "The track skip slice has been reset, so the length should be zero.")
|
||||||
|
suite.Equal(2, suite.Skips.NumPlaylistSkips(), "The playlist skip slice should be unaffected.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *SkipTrackerTestSuite) TestResetPlaylistSkips() {
|
||||||
|
suite.Skips.AddTrackSkip(suite.User1)
|
||||||
|
suite.Skips.AddTrackSkip(suite.User2)
|
||||||
|
suite.Skips.AddPlaylistSkip(suite.User1)
|
||||||
|
suite.Skips.AddPlaylistSkip(suite.User2)
|
||||||
|
|
||||||
|
suite.Equal(2, suite.Skips.NumTrackSkips(), "There should be two users in the track skip slice.")
|
||||||
|
suite.Equal(2, suite.Skips.NumPlaylistSkips(), "There should be two users in the playlist skip slice.")
|
||||||
|
|
||||||
|
suite.Skips.ResetPlaylistSkips()
|
||||||
|
|
||||||
|
suite.Equal(2, suite.Skips.NumTrackSkips(), "The track skip slice should be unaffected.")
|
||||||
|
suite.Zero(suite.Skips.NumPlaylistSkips(), "The playlist skip slice has been reset, so the length should be zero.")
|
||||||
|
}*/
|
||||||
|
|
||||||
|
func TestSkipTrackerTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(SkipTrackerTestSuite))
|
||||||
|
}
|
97
bot/startup.go
Normal file
97
bot/startup.go
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* bot/startup.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PerformStartupChecks executes the suite of startup checks that are run before the bot
|
||||||
|
// connects to the server.
|
||||||
|
func PerformStartupChecks() {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"num_services": fmt.Sprintf("%d", len(DJ.AvailableServices)),
|
||||||
|
}).Infoln("Checking for availability of services...")
|
||||||
|
|
||||||
|
for i := len(DJ.AvailableServices) - 1; i >= 0; i-- {
|
||||||
|
if err := DJ.AvailableServices[i].CheckAPIKey(); err != nil {
|
||||||
|
name := DJ.AvailableServices[i].GetReadableName()
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"service": name,
|
||||||
|
"error": err.Error(),
|
||||||
|
}).Warnln("A startup check discovered an issue. The service will be disabled.")
|
||||||
|
|
||||||
|
// Remove service from enabled services.
|
||||||
|
DJ.AvailableServices = append(DJ.AvailableServices[:i], DJ.AvailableServices[i+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(DJ.AvailableServices) == 0 {
|
||||||
|
logrus.Fatalln("The bot cannot continue as no services are enabled.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := checkYouTubeDLInstallation(); err != nil {
|
||||||
|
logrus.Fatalln("youtube-dl is either not installed or is not discoverable in $PATH. youtube-dl is required to download audio.")
|
||||||
|
}
|
||||||
|
if viper.GetString("defaults.player_command") == "ffmpeg" {
|
||||||
|
if err := checkFfmpegInstallation(); err != nil {
|
||||||
|
logrus.Fatalln("ffmpeg is either not installed or is not discoverable in $PATH. If you would like to use avconv instead, change the defaults.player_command value in the configuration file.")
|
||||||
|
}
|
||||||
|
} else if viper.GetString("defaults.player_command") == "avconv" {
|
||||||
|
if err := checkAvconvInstallation(); err != nil {
|
||||||
|
logrus.Fatalln("avconv is either not installed or is not discoverable in $PATH. If you would like to use ffmpeg instead, change the defaults.player_command value in the configuration file.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logrus.Fatalln("The player command provided in the configuration file is invalid. Valid choices are: \"ffmpeg\", \"avconv\".")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := checkAria2Installation(); err != nil {
|
||||||
|
logrus.Warnln("aria2 is not installed or is not discoverable in $PATH. The bot will still partially work, but some services will not work properly.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkYouTubeDLInstallation() error {
|
||||||
|
logrus.Infoln("Checking YouTubeDL installation...")
|
||||||
|
command := exec.Command("youtube-dl", "--version")
|
||||||
|
if err := command.Run(); err != nil {
|
||||||
|
return errors.New("youtube-dl is not properly installed")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkFfmpegInstallation() error {
|
||||||
|
logrus.Infoln("Checking ffmpeg installation...")
|
||||||
|
command := exec.Command("ffmpeg", "-version")
|
||||||
|
if err := command.Run(); err != nil {
|
||||||
|
return errors.New("ffmpeg is not properly installed")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkAvconvInstallation() error {
|
||||||
|
logrus.Infoln("Checking avconv installation...")
|
||||||
|
command := exec.Command("avconv", "-version")
|
||||||
|
if err := command.Run(); err != nil {
|
||||||
|
return errors.New("avconv is not properly installed")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkAria2Installation() error {
|
||||||
|
logrus.Infoln("Checking aria2c installation...")
|
||||||
|
command := exec.Command("aria2c", "-v")
|
||||||
|
if err := command.Run(); err != nil {
|
||||||
|
return errors.New("aria2c is not properly installed")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
94
bot/track.go
Normal file
94
bot/track.go
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* bot/track.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/matthieugrieger/mumbledj/interfaces"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Track stores all metadata related to an audio track.
|
||||||
|
type Track struct {
|
||||||
|
ID string
|
||||||
|
URL string
|
||||||
|
Title string
|
||||||
|
Author string
|
||||||
|
AuthorURL string
|
||||||
|
Submitter string
|
||||||
|
Service string
|
||||||
|
Filename string
|
||||||
|
ThumbnailURL string
|
||||||
|
Duration time.Duration
|
||||||
|
PlaybackOffset time.Duration
|
||||||
|
Playlist interfaces.Playlist
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the ID of the track.
|
||||||
|
func (t Track) GetID() string {
|
||||||
|
return t.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetURL returns the URL of the track.
|
||||||
|
func (t Track) GetURL() string {
|
||||||
|
return t.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTitle returns the title of the track.
|
||||||
|
func (t Track) GetTitle() string {
|
||||||
|
return t.Title
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAuthor returns the author of the track.
|
||||||
|
func (t Track) GetAuthor() string {
|
||||||
|
return t.Author
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAuthorURL returns the URL that links to the author of the track.
|
||||||
|
func (t Track) GetAuthorURL() string {
|
||||||
|
return t.AuthorURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSubmitter returns the submitter of the track.
|
||||||
|
func (t Track) GetSubmitter() string {
|
||||||
|
return t.Submitter
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetService returns the name of the service from which the track was retrieved from.
|
||||||
|
func (t Track) GetService() string {
|
||||||
|
return t.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFilename returns the name of the file stored on disk, if it exists. If no
|
||||||
|
// file on disk exists an empty string and error are returned.
|
||||||
|
func (t Track) GetFilename() string {
|
||||||
|
return t.Filename
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetThumbnailURL returns the URL to the thumbnail for the track. If no thumbnail
|
||||||
|
// exists an empty string and error are returned.
|
||||||
|
func (t Track) GetThumbnailURL() string {
|
||||||
|
return t.ThumbnailURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDuration returns the duration of the track.
|
||||||
|
func (t Track) GetDuration() time.Duration {
|
||||||
|
return t.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPlaybackOffset returns the playback offset for the track. A duration
|
||||||
|
// of 0 is given to tracks that do not specify an offset.
|
||||||
|
func (t Track) GetPlaybackOffset() time.Duration {
|
||||||
|
return t.PlaybackOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPlaylist returns the playlist the track is associated with, if it exists. If
|
||||||
|
// the track is not associated with a playlist a nil playlist and error are returned.
|
||||||
|
func (t Track) GetPlaylist() interfaces.Playlist {
|
||||||
|
return t.Playlist
|
||||||
|
}
|
107
bot/track_test.go
Normal file
107
bot/track_test.go
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* bot/track_test.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TrackTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
Track Track
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TrackTestSuite) SetupTest() {
|
||||||
|
duration, _ := time.ParseDuration("1s")
|
||||||
|
suite.Track = Track{
|
||||||
|
ID: "id",
|
||||||
|
Title: "title",
|
||||||
|
Author: "author",
|
||||||
|
Submitter: "submitter",
|
||||||
|
Service: "service",
|
||||||
|
Filename: "filename",
|
||||||
|
ThumbnailURL: "thumbnailurl",
|
||||||
|
Duration: duration,
|
||||||
|
Playlist: new(Playlist),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TrackTestSuite) TestGetID() {
|
||||||
|
suite.Equal("id", suite.Track.GetID())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TrackTestSuite) TestGetTitle() {
|
||||||
|
suite.Equal("title", suite.Track.GetTitle())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TrackTestSuite) TestGetAuthor() {
|
||||||
|
suite.Equal("author", suite.Track.GetAuthor())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TrackTestSuite) TestGetSubmitter() {
|
||||||
|
suite.Equal("submitter", suite.Track.GetSubmitter())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TrackTestSuite) TestGetService() {
|
||||||
|
suite.Equal("service", suite.Track.GetService())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TrackTestSuite) TestGetFilenameWhenExists() {
|
||||||
|
result := suite.Track.GetFilename()
|
||||||
|
|
||||||
|
suite.Equal("filename", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TrackTestSuite) TestGetFilenameWhenNotExists() {
|
||||||
|
suite.Track.Filename = ""
|
||||||
|
|
||||||
|
result := suite.Track.GetFilename()
|
||||||
|
|
||||||
|
suite.Equal("", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TrackTestSuite) TestGetThumbnailURLWhenExists() {
|
||||||
|
result := suite.Track.GetThumbnailURL()
|
||||||
|
|
||||||
|
suite.Equal("thumbnailurl", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TrackTestSuite) TestGetThumbnailURLWhenNotExists() {
|
||||||
|
suite.Track.ThumbnailURL = ""
|
||||||
|
|
||||||
|
result := suite.Track.GetThumbnailURL()
|
||||||
|
|
||||||
|
suite.Equal("", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TrackTestSuite) TestGetDuration() {
|
||||||
|
duration, _ := time.ParseDuration("1s")
|
||||||
|
|
||||||
|
suite.Equal(duration, suite.Track.GetDuration())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TrackTestSuite) TestGetPlaylistWhenExists() {
|
||||||
|
result := suite.Track.GetPlaylist()
|
||||||
|
|
||||||
|
suite.NotNil(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TrackTestSuite) TestGetPlaylistWhenNotExists() {
|
||||||
|
suite.Track.Playlist = nil
|
||||||
|
|
||||||
|
result := suite.Track.GetPlaylist()
|
||||||
|
|
||||||
|
suite.Nil(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrackTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(TrackTestSuite))
|
||||||
|
}
|
81
bot/youtube_dl.go
Normal file
81
bot/youtube_dl.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* bot/youtube_dl.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package bot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/matthieugrieger/mumbledj/interfaces"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// YouTubeDL is a struct that gathers all methods related to the youtube-dl
|
||||||
|
// software.
|
||||||
|
// youtube-dl: https://rg3.github.io/youtube-dl/
|
||||||
|
type YouTubeDL struct{}
|
||||||
|
|
||||||
|
// Download downloads the audio associated with the incoming `track` object
|
||||||
|
// and stores it `track.Filename`.
|
||||||
|
func (yt *YouTubeDL) Download(t interfaces.Track) error {
|
||||||
|
player := "--prefer-ffmpeg"
|
||||||
|
if viper.GetString("defaults.player_command") == "avconv" {
|
||||||
|
player = "--prefer-avconv"
|
||||||
|
}
|
||||||
|
|
||||||
|
filepath := os.ExpandEnv(viper.GetString("cache.directory") + "/" + t.GetFilename())
|
||||||
|
|
||||||
|
// Determine which format to use.
|
||||||
|
format := "bestaudio"
|
||||||
|
for _, service := range DJ.AvailableServices {
|
||||||
|
if service.GetReadableName() == t.GetService() {
|
||||||
|
format = service.GetFormat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check to see if track is already downloaded.
|
||||||
|
if _, err := os.Stat(filepath); os.IsNotExist(err) {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
if t.GetService() == "Mixcloud" {
|
||||||
|
cmd = exec.Command("youtube-dl", "--verbose", "--no-mtime", "--output", filepath, "--format", format, "--external-downloader", "aria2c", player, t.GetURL())
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command("youtube-dl", "--verbose", "--no-mtime", "--output", filepath, "--format", format, player, t.GetURL())
|
||||||
|
}
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
args := ""
|
||||||
|
for s := range cmd.Args {
|
||||||
|
args += cmd.Args[s] + " "
|
||||||
|
}
|
||||||
|
logrus.Warnf("%s\n%s\nyoutube-dl: %s", args, string(output), err.Error())
|
||||||
|
return errors.New("Track download failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if viper.GetBool("cache.enabled") {
|
||||||
|
DJ.Cache.CheckDirectorySize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete deletes the audio file associated with the incoming `track` object.
|
||||||
|
func (yt *YouTubeDL) Delete(t interfaces.Track) error {
|
||||||
|
if !viper.GetBool("cache.enabled") {
|
||||||
|
filePath := os.ExpandEnv(viper.GetString("cache.directory") + "/" + t.GetFilename())
|
||||||
|
if _, err := os.Stat(filePath); err == nil {
|
||||||
|
if err := os.Remove(filePath); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("An error occurred while deleting the audio file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
113
cache.go
113
cache.go
|
@ -1,113 +0,0 @@
|
||||||
/*
|
|
||||||
* MumbleDJ
|
|
||||||
* By Matthieu Grieger
|
|
||||||
* cache.go
|
|
||||||
* Copyright (c) 2014, 2015 Matthieu Grieger (MIT License)
|
|
||||||
*/
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ByAge is a type that holds file information for the cache items.
|
|
||||||
type ByAge []os.FileInfo
|
|
||||||
|
|
||||||
func (a ByAge) Len() int {
|
|
||||||
return len(a)
|
|
||||||
}
|
|
||||||
func (a ByAge) Swap(i, j int) {
|
|
||||||
a[i], a[j] = a[j], a[i]
|
|
||||||
}
|
|
||||||
func (a ByAge) Less(i, j int) bool {
|
|
||||||
return time.Since(a[i].ModTime()) < time.Since(a[j].ModTime())
|
|
||||||
}
|
|
||||||
|
|
||||||
// SongCache is a struct that holds the number of songs currently cached and
|
|
||||||
// their combined file size.
|
|
||||||
type SongCache struct {
|
|
||||||
NumSongs int
|
|
||||||
TotalFileSize int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSongCache creates an empty SongCache.
|
|
||||||
func NewSongCache() *SongCache {
|
|
||||||
newCache := &SongCache{
|
|
||||||
NumSongs: 0,
|
|
||||||
TotalFileSize: 0,
|
|
||||||
}
|
|
||||||
return newCache
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNumSongs returns the number of songs currently cached.
|
|
||||||
func (c *SongCache) GetNumSongs() int {
|
|
||||||
songs, _ := ioutil.ReadDir(fmt.Sprintf("%s/.mumbledj/songs", dj.homeDir))
|
|
||||||
return len(songs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCurrentTotalFileSize calculates the total file size of the files within
|
|
||||||
// the cache and returns it.
|
|
||||||
func (c *SongCache) GetCurrentTotalFileSize() int64 {
|
|
||||||
var totalSize int64
|
|
||||||
songs, _ := ioutil.ReadDir(fmt.Sprintf("%s/.mumbledj/songs", dj.homeDir))
|
|
||||||
for _, song := range songs {
|
|
||||||
totalSize += song.Size()
|
|
||||||
}
|
|
||||||
return totalSize
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckMaximumDirectorySize checks the cache directory to determine if the filesize
|
|
||||||
// of the songs within exceed the user-specified size limit. If so, the oldest files
|
|
||||||
// get cleared until it is no longer exceeding the limit.
|
|
||||||
func (c *SongCache) CheckMaximumDirectorySize() {
|
|
||||||
for c.GetCurrentTotalFileSize() > (dj.conf.Cache.MaximumSize * 1048576) {
|
|
||||||
if err := c.ClearOldest(); err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update updates the SongCache struct.
|
|
||||||
func (c *SongCache) Update() {
|
|
||||||
c.NumSongs = c.GetNumSongs()
|
|
||||||
c.TotalFileSize = c.GetCurrentTotalFileSize()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearExpired clears cache items that are older than the cache period set within
|
|
||||||
// the user configuration.
|
|
||||||
func (c *SongCache) ClearExpired() {
|
|
||||||
for range time.Tick(5 * time.Minute) {
|
|
||||||
songs, _ := ioutil.ReadDir(fmt.Sprintf("%s/.mumbledj/songs", dj.homeDir))
|
|
||||||
for _, song := range songs {
|
|
||||||
hours := time.Since(song.ModTime()).Hours()
|
|
||||||
if hours >= dj.conf.Cache.ExpireTime {
|
|
||||||
if dj.queue.Len() > 0 {
|
|
||||||
if (dj.queue.CurrentSong().Filename()) != song.Name() {
|
|
||||||
os.Remove(fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, song.Name()))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
os.Remove(fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, song.Name()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearOldest deletes the oldest item in the cache.
|
|
||||||
func (c *SongCache) ClearOldest() error {
|
|
||||||
songs, _ := ioutil.ReadDir(fmt.Sprintf("%s/.mumbledj/songs", dj.homeDir))
|
|
||||||
sort.Sort(ByAge(songs))
|
|
||||||
if dj.queue.Len() > 0 {
|
|
||||||
if (dj.queue.CurrentSong().Filename()) != songs[0].Name() {
|
|
||||||
return os.Remove(fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, songs[0].Name()))
|
|
||||||
}
|
|
||||||
return errors.New("Song is currently playing.")
|
|
||||||
}
|
|
||||||
return os.Remove(fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, songs[0].Name()))
|
|
||||||
}
|
|
534
commands.go
534
commands.go
|
@ -1,534 +0,0 @@
|
||||||
/*
|
|
||||||
* MumbleDJ
|
|
||||||
* By Matthieu Grieger
|
|
||||||
* commands.go
|
|
||||||
* Copyright (c) 2014, 2015 Matthieu Grieger (MIT License)
|
|
||||||
*/
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/layeh/gumble/gumble"
|
|
||||||
)
|
|
||||||
|
|
||||||
// parseCommand views incoming chat messages and determines if there is a valid command within them.
|
|
||||||
// If a command exists, the arguments (if any) will be parsed and sent to the appropriate helper
|
|
||||||
// function to perform the command's task.
|
|
||||||
func parseCommand(user *gumble.User, username, command string) {
|
|
||||||
var com, argument string
|
|
||||||
split := strings.Split(command, "\n")
|
|
||||||
splitString := split[0]
|
|
||||||
if strings.Contains(splitString, " ") {
|
|
||||||
index := strings.Index(splitString, " ")
|
|
||||||
com, argument = splitString[0:index], splitString[(index+1):]
|
|
||||||
} else {
|
|
||||||
com = command
|
|
||||||
argument = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
switch com {
|
|
||||||
// Add command
|
|
||||||
case dj.conf.Aliases.AddAlias:
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminAdd) {
|
|
||||||
add(user, argument)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
// Addnext command
|
|
||||||
case dj.conf.Aliases.AddNextAlias:
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminAddNext) {
|
|
||||||
addNext(user, argument)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
// Skip command
|
|
||||||
case dj.conf.Aliases.SkipAlias:
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminSkip) {
|
|
||||||
skip(user, false, false)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
// Skip playlist command
|
|
||||||
case dj.conf.Aliases.SkipPlaylistAlias:
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminAddPlaylists) {
|
|
||||||
skip(user, false, true)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
// Forceskip command
|
|
||||||
case dj.conf.Aliases.AdminSkipAlias:
|
|
||||||
if dj.HasPermission(username, true) {
|
|
||||||
skip(user, true, false)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
// Playlist forceskip command
|
|
||||||
case dj.conf.Aliases.AdminSkipPlaylistAlias:
|
|
||||||
if dj.HasPermission(username, true) {
|
|
||||||
skip(user, true, true)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
// Help command
|
|
||||||
case dj.conf.Aliases.HelpAlias:
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminHelp) {
|
|
||||||
help(user)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
// Volume command
|
|
||||||
case dj.conf.Aliases.VolumeAlias:
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminVolume) {
|
|
||||||
volume(user, argument)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
// Move command
|
|
||||||
case dj.conf.Aliases.MoveAlias:
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminMove) {
|
|
||||||
move(user, argument)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
// JoinMe command
|
|
||||||
case dj.conf.Aliases.JoinMeAlias:
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminJoinMe) {
|
|
||||||
joinMe(user)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
// Reload command
|
|
||||||
case dj.conf.Aliases.ReloadAlias:
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminReload) {
|
|
||||||
reload(user)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
// Reset command
|
|
||||||
case dj.conf.Aliases.ResetAlias:
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminReset) {
|
|
||||||
reset(username)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
// Numsongs command
|
|
||||||
case dj.conf.Aliases.NumSongsAlias:
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminNumSongs) {
|
|
||||||
numSongs()
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
// Nextsong command
|
|
||||||
case dj.conf.Aliases.NextSongAlias:
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminNextSong) {
|
|
||||||
nextSong(user)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
// Currentsong command
|
|
||||||
case dj.conf.Aliases.CurrentSongAlias:
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminCurrentSong) {
|
|
||||||
currentSong(user)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
// Setcomment command
|
|
||||||
case dj.conf.Aliases.SetCommentAlias:
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminSetComment) {
|
|
||||||
setComment(user, argument)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
// Numcached command
|
|
||||||
case dj.conf.Aliases.NumCachedAlias:
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminNumCached) {
|
|
||||||
numCached(user)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
// Cachesize command
|
|
||||||
case dj.conf.Aliases.CacheSizeAlias:
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminCacheSize) {
|
|
||||||
cacheSize(user)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
// Kill command
|
|
||||||
case dj.conf.Aliases.KillAlias:
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminKill) {
|
|
||||||
kill()
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shuffle command
|
|
||||||
case dj.conf.Aliases.ShuffleAlias:
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminShuffle) {
|
|
||||||
shuffleSongs(user, username)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shuffleon command
|
|
||||||
case dj.conf.Aliases.ShuffleOnAlias:
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminShuffleToggle) {
|
|
||||||
toggleAutomaticShuffle(true, user, username)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shuffleoff command
|
|
||||||
case dj.conf.Aliases.ShuffleOffAlias:
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminShuffleToggle) {
|
|
||||||
toggleAutomaticShuffle(false, user, username)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListSongs command
|
|
||||||
case dj.conf.Aliases.ListSongsAlias:
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminListSongs) {
|
|
||||||
listSongs(user, argument)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Version command
|
|
||||||
case dj.conf.Aliases.VersionAlias:
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminVersion) {
|
|
||||||
version(user)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
dj.SendPrivateMessage(user, COMMAND_DOESNT_EXIST_MSG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add performs !add functionality. Checks input URL for service, and adds
|
|
||||||
// the URL to the queue if the format matches.
|
|
||||||
func add(user *gumble.User, url string) error {
|
|
||||||
if url == "" {
|
|
||||||
dj.SendPrivateMessage(user, NO_ARGUMENT_MSG)
|
|
||||||
return errors.New("NO_ARGUMENT")
|
|
||||||
} else {
|
|
||||||
err := FindServiceAndAdd(user, url)
|
|
||||||
if err != nil {
|
|
||||||
dj.SendPrivateMessage(user, err.Error())
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// addnext performs !addnext functionality. Checks input URL for service, and adds
|
|
||||||
// the URL to the queue as the next song if the format matches.
|
|
||||||
func addNext(user *gumble.User, url string) error {
|
|
||||||
if !dj.audioStream.IsPlaying() {
|
|
||||||
return add(user, url)
|
|
||||||
} else {
|
|
||||||
if url == "" {
|
|
||||||
dj.SendPrivateMessage(user, NO_ARGUMENT_MSG)
|
|
||||||
return errors.New("NO_ARGUMENT")
|
|
||||||
} else {
|
|
||||||
err := FindServiceAndInsertNext(user, url)
|
|
||||||
if err != nil {
|
|
||||||
dj.SendPrivateMessage(user, err.Error())
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// skip performs !skip functionality. Adds a skip to the skippers slice for the current song, and then
|
|
||||||
// evaluates if a skip should be performed. Both skip and forceskip are implemented here.
|
|
||||||
func skip(user *gumble.User, admin, playlistSkip bool) {
|
|
||||||
if dj.audioStream.IsPlaying() {
|
|
||||||
if playlistSkip {
|
|
||||||
if dj.queue.CurrentSong().Playlist() != nil {
|
|
||||||
if err := dj.queue.CurrentSong().Playlist().AddSkip(user.Name); err == nil {
|
|
||||||
submitterSkipped := false
|
|
||||||
if admin {
|
|
||||||
dj.client.Self.Channel.Send(ADMIN_PLAYLIST_SKIP_MSG, false)
|
|
||||||
} else if dj.queue.CurrentSong().Submitter() == user.Name {
|
|
||||||
dj.client.Self.Channel.Send(fmt.Sprintf(PLAYLIST_SUBMITTER_SKIP_HTML, user.Name), false)
|
|
||||||
submitterSkipped = true
|
|
||||||
} else {
|
|
||||||
dj.client.Self.Channel.Send(fmt.Sprintf(PLAYLIST_SKIP_ADDED_HTML, user.Name), false)
|
|
||||||
}
|
|
||||||
if submitterSkipped || dj.queue.CurrentSong().Playlist().SkipReached(len(dj.client.Self.Channel.Users)) || admin {
|
|
||||||
id := dj.queue.CurrentSong().Playlist().ID()
|
|
||||||
dj.queue.CurrentSong().Playlist().DeleteSkippers()
|
|
||||||
for i := 0; i < len(dj.queue.queue); i++ {
|
|
||||||
if dj.queue.queue[i].Playlist() != nil {
|
|
||||||
if dj.queue.queue[i].Playlist().ID() == id {
|
|
||||||
dj.queue.queue = append(dj.queue.queue[:i], dj.queue.queue[i+1:]...)
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if dj.queue.Len() != 0 {
|
|
||||||
// Set dontSkip to true to avoid audioStream.Stop() callback skipping the new first song.
|
|
||||||
dj.queue.CurrentSong().SetDontSkip(true)
|
|
||||||
}
|
|
||||||
if !(submitterSkipped || admin) {
|
|
||||||
dj.client.Self.Channel.Send(PLAYLIST_SKIPPED_HTML, false)
|
|
||||||
}
|
|
||||||
if err := dj.audioStream.Stop(); err != nil {
|
|
||||||
panic(errors.New("An error occurred while stopping the current song."))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PLAYLIST_PLAYING_MSG)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := dj.queue.CurrentSong().AddSkip(user.Name); err == nil {
|
|
||||||
submitterSkipped := false
|
|
||||||
if admin {
|
|
||||||
dj.client.Self.Channel.Send(ADMIN_SONG_SKIP_MSG, false)
|
|
||||||
} else if dj.queue.CurrentSong().Submitter() == user.Name {
|
|
||||||
dj.client.Self.Channel.Send(fmt.Sprintf(SUBMITTER_SKIP_HTML, user.Name), false)
|
|
||||||
submitterSkipped = true
|
|
||||||
} else {
|
|
||||||
dj.client.Self.Channel.Send(fmt.Sprintf(SKIP_ADDED_HTML, user.Name), false)
|
|
||||||
}
|
|
||||||
if submitterSkipped || dj.queue.CurrentSong().SkipReached(len(dj.client.Self.Channel.Users)) || admin {
|
|
||||||
if !(submitterSkipped || admin) {
|
|
||||||
dj.client.Self.Channel.Send(SONG_SKIPPED_HTML, false)
|
|
||||||
}
|
|
||||||
if err := dj.audioStream.Stop(); err != nil {
|
|
||||||
panic(errors.New("An error occurred while stopping the current song."))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_MUSIC_PLAYING_MSG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// help performs !help functionality. Displays a list of valid commands.
|
|
||||||
func help(user *gumble.User) {
|
|
||||||
dj.SendPrivateMessage(user, HELP_HTML)
|
|
||||||
}
|
|
||||||
|
|
||||||
// volume performs !volume functionality. Checks input value against LowestVolume and HighestVolume from
|
|
||||||
// config to determine if the volume should be applied. If in the correct range, the new volume
|
|
||||||
// is applied and is immediately in effect.
|
|
||||||
func volume(user *gumble.User, value string) {
|
|
||||||
if value == "" {
|
|
||||||
dj.client.Self.Channel.Send(fmt.Sprintf(CUR_VOLUME_HTML, dj.audioStream.Volume), false)
|
|
||||||
} else {
|
|
||||||
if parsedVolume, err := strconv.ParseFloat(value, 32); err == nil {
|
|
||||||
newVolume := float32(parsedVolume)
|
|
||||||
if newVolume >= dj.conf.Volume.LowestVolume && newVolume <= dj.conf.Volume.HighestVolume {
|
|
||||||
dj.audioStream.Volume = newVolume
|
|
||||||
dj.client.Self.Channel.Send(fmt.Sprintf(VOLUME_SUCCESS_HTML, user.Name, dj.audioStream.Volume), false)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, fmt.Sprintf(NOT_IN_VOLUME_RANGE_MSG, dj.conf.Volume.LowestVolume, dj.conf.Volume.HighestVolume))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, fmt.Sprintf(NOT_IN_VOLUME_RANGE_MSG, dj.conf.Volume.LowestVolume, dj.conf.Volume.HighestVolume))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// move performs !move functionality. Determines if the supplied channel is valid and moves the bot
|
|
||||||
// to the channel if it is.
|
|
||||||
func move(user *gumble.User, channel string) {
|
|
||||||
if channel == "" {
|
|
||||||
dj.SendPrivateMessage(user, NO_ARGUMENT_MSG)
|
|
||||||
} else {
|
|
||||||
if channels := strings.Split(channel, "/"); dj.client.Channels.Find(channels...) != nil {
|
|
||||||
dj.client.Self.Move(dj.client.Channels.Find(channels...))
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, CHANNEL_DOES_NOT_EXIST_MSG+" "+channel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// joinMe performs !joinme functionality. Finds the channel and moves the bot.
|
|
||||||
// The bot does not move if it is already playing audio to others.
|
|
||||||
func joinMe(user *gumble.User) {
|
|
||||||
if dj.audioStream.IsPlaying() && len(dj.client.Self.Channel.Users) > 1 {
|
|
||||||
user.Send(PEOPLE_ARE_LISTENING_TO_ME)
|
|
||||||
} else {
|
|
||||||
dj.client.Self.Move(user.Channel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// reload performs !reload functionality. Tells command submitter if the reload completed successfully.
|
|
||||||
func reload(user *gumble.User) {
|
|
||||||
if err := loadConfiguration(); err == nil {
|
|
||||||
dj.SendPrivateMessage(user, CONFIG_RELOAD_SUCCESS_MSG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// reset performs !reset functionality. Clears the song queue, stops playing audio, and deletes all
|
|
||||||
// remaining songs in the ~/.mumbledj/songs directory.
|
|
||||||
func reset(username string) {
|
|
||||||
dj.queue.queue = dj.queue.queue[:0]
|
|
||||||
if dj.audioStream.IsPlaying() {
|
|
||||||
if err := dj.audioStream.Stop(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := deleteSongs(); err == nil {
|
|
||||||
dj.client.Self.Channel.Send(fmt.Sprintf(QUEUE_RESET_HTML, username), false)
|
|
||||||
} else {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// numSongs performs !numsongs functionality. Uses the SongQueue traversal function to traverse the
|
|
||||||
// queue with a function call that increments a counter. Once finished, the bot outputs
|
|
||||||
// the number of songs in the queue to chat.
|
|
||||||
func numSongs() {
|
|
||||||
songCount := 0
|
|
||||||
dj.queue.Traverse(func(i int, song Song) {
|
|
||||||
songCount++
|
|
||||||
})
|
|
||||||
dj.client.Self.Channel.Send(fmt.Sprintf(NUM_SONGS_HTML, songCount), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// nextSong performs !nextsong functionality. Uses the SongQueue PeekNext function to peek at the next
|
|
||||||
// item if it exists. The user will then be sent a message containing the title and submitter
|
|
||||||
// of the next item if it exists.
|
|
||||||
func nextSong(user *gumble.User) {
|
|
||||||
if song, err := dj.queue.PeekNext(); err != nil {
|
|
||||||
dj.SendPrivateMessage(user, NO_SONG_NEXT_MSG)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, fmt.Sprintf(NEXT_SONG_HTML, song.Title(), song.Submitter()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// currentSong performs !currentsong functionality. Sends the user who submitted the currentsong command
|
|
||||||
// information about the song currently playing.
|
|
||||||
func currentSong(user *gumble.User) {
|
|
||||||
if dj.audioStream.IsPlaying() {
|
|
||||||
if dj.queue.CurrentSong().Playlist() == nil {
|
|
||||||
dj.SendPrivateMessage(user, fmt.Sprintf(CURRENT_SONG_HTML, dj.queue.CurrentSong().Title(), dj.queue.CurrentSong().Submitter()))
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, fmt.Sprintf(CURRENT_SONG_PLAYLIST_HTML, dj.queue.CurrentSong().Title(),
|
|
||||||
dj.queue.CurrentSong().Submitter(), dj.queue.CurrentSong().Playlist().Title()))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_MUSIC_PLAYING_MSG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// setComment performs !setcomment functionality. Sets the bot's comment to whatever text is supplied in the argument.
|
|
||||||
func setComment(user *gumble.User, comment string) {
|
|
||||||
dj.client.Self.SetComment(comment)
|
|
||||||
dj.SendPrivateMessage(user, COMMENT_UPDATED_MSG)
|
|
||||||
}
|
|
||||||
|
|
||||||
// numCached performs !numcached functionality. Displays the number of songs currently cached on disk at ~/.mumbledj/songs.
|
|
||||||
func numCached(user *gumble.User) {
|
|
||||||
if dj.conf.Cache.Enabled {
|
|
||||||
dj.cache.Update()
|
|
||||||
dj.SendPrivateMessage(user, fmt.Sprintf(NUM_CACHED_MSG, dj.cache.NumSongs))
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, CACHE_NOT_ENABLED_MSG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// cacheSize performs !cachesize functionality. Displays the total file size of the cached audio files.
|
|
||||||
func cacheSize(user *gumble.User) {
|
|
||||||
if dj.conf.Cache.Enabled {
|
|
||||||
dj.cache.Update()
|
|
||||||
dj.SendPrivateMessage(user, fmt.Sprintf(CACHE_SIZE_MSG, float64(dj.cache.TotalFileSize/1048576)))
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, CACHE_NOT_ENABLED_MSG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// kill performs !kill functionality. First cleans the ~/.mumbledj/songs directory to get rid of any
|
|
||||||
// excess m4a files. The bot then safely disconnects from the server.
|
|
||||||
func kill() {
|
|
||||||
if err := deleteSongs(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if err := dj.client.Disconnect(); err == nil {
|
|
||||||
fmt.Println("Kill successful. Goodbye!")
|
|
||||||
os.Exit(0)
|
|
||||||
} else {
|
|
||||||
panic(errors.New("An error occurred while disconnecting from the server."))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteSongs deletes songs from ~/.mumbledj/songs.
|
|
||||||
func deleteSongs() error {
|
|
||||||
songsDir := fmt.Sprintf("%s/.mumbledj/songs", dj.homeDir)
|
|
||||||
if err := os.RemoveAll(songsDir); err != nil {
|
|
||||||
return errors.New("An error occurred while deleting the audio files.")
|
|
||||||
}
|
|
||||||
if err := os.Mkdir(songsDir, 0777); err != nil {
|
|
||||||
return errors.New("An error occurred while recreating the songs directory.")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// shuffles the song list
|
|
||||||
func shuffleSongs(user *gumble.User, username string) {
|
|
||||||
if dj.queue.Len() > 1 {
|
|
||||||
dj.queue.ShuffleSongs()
|
|
||||||
dj.client.Self.Channel.Send(fmt.Sprintf(SHUFFLE_SUCCESS_MSG, username), false)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, CANT_SHUFFLE_MSG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handles toggling of automatic shuffle playing
|
|
||||||
func toggleAutomaticShuffle(activate bool, user *gumble.User, username string) {
|
|
||||||
if dj.conf.General.AutomaticShuffleOn != activate {
|
|
||||||
dj.conf.General.AutomaticShuffleOn = activate
|
|
||||||
if activate {
|
|
||||||
dj.client.Self.Channel.Send(fmt.Sprintf(SHUFFLE_ON_MESSAGE, username), false)
|
|
||||||
} else {
|
|
||||||
dj.client.Self.Channel.Send(fmt.Sprintf(SHUFFLE_OFF_MESSAGE, username), false)
|
|
||||||
}
|
|
||||||
} else if activate {
|
|
||||||
dj.SendPrivateMessage(user, SHUFFLE_ACTIVATED_ERROR_MESSAGE)
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, SHUFFLE_DEACTIVATED_ERROR_MESSAGE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// listSongs handles !listSongs functionality. Sends a private message to the user a list of all songs in the queue
|
|
||||||
func listSongs(user *gumble.User, value string) {
|
|
||||||
if dj.audioStream.IsPlaying() {
|
|
||||||
num := 0
|
|
||||||
if value == "" {
|
|
||||||
num = dj.queue.Len()
|
|
||||||
} else {
|
|
||||||
if parsedNum, err := strconv.Atoi(value); err != nil {
|
|
||||||
num = dj.queue.Len()
|
|
||||||
} else {
|
|
||||||
num = parsedNum
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
dj.queue.Traverse(func(i int, song Song) {
|
|
||||||
if i < num {
|
|
||||||
buffer.WriteString(fmt.Sprintf(SONG_LIST_HTML, i+1, song.Title(), song.Submitter()))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
dj.SendPrivateMessage(user, buffer.String())
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_MUSIC_PLAYING_MSG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// version handles !version functionality. Sends a private message to the user with the most recent version of MumbleDJ
|
|
||||||
func version(user *gumble.User) {
|
|
||||||
dj.SendPrivateMessage(user, DJ_VERSION)
|
|
||||||
}
|
|
||||||
|
|
97
commands/add.go
Normal file
97
commands/add.go
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/add.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/matthieugrieger/mumbledj/interfaces"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddCommand is a command that adds an audio track associated with a supported
|
||||||
|
// URL to the queue.
|
||||||
|
type AddCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *AddCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.add.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *AddCommand) Description() string {
|
||||||
|
return viper.GetString("commands.add.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *AddCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.add.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *AddCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
var (
|
||||||
|
allTracks []interfaces.Track
|
||||||
|
tracks []interfaces.Track
|
||||||
|
service interfaces.Service
|
||||||
|
err error
|
||||||
|
lastTrackAdded interfaces.Track
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
return "", true, errors.New("A URL must be supplied with the add command")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, arg := range args {
|
||||||
|
if service, err = DJ.GetService(arg); err == nil {
|
||||||
|
tracks, err = service.GetTracks(arg, user)
|
||||||
|
if err == nil {
|
||||||
|
allTracks = append(allTracks, tracks...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(allTracks) == 0 {
|
||||||
|
return "", true, errors.New("No valid tracks were found with the provided URL(s)")
|
||||||
|
}
|
||||||
|
|
||||||
|
numTooLong := 0
|
||||||
|
numAdded := 0
|
||||||
|
for _, track := range allTracks {
|
||||||
|
if err = DJ.Queue.AppendTrack(track); err != nil {
|
||||||
|
numTooLong++
|
||||||
|
} else {
|
||||||
|
numAdded++
|
||||||
|
lastTrackAdded = track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if numAdded == 0 {
|
||||||
|
return "", true, errors.New("Your track(s) were either too long or an error occurred while processing them. No track(s) have been added.")
|
||||||
|
} else if numAdded == 1 {
|
||||||
|
return fmt.Sprintf("<b>%s</b> added <b>1</b> track to the queue:<br>\"%s\" from %s",
|
||||||
|
user.Name, lastTrackAdded.GetTitle(), lastTrackAdded.GetService()), false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
retString := fmt.Sprintf("<b>%s</b> added <b>%d</b> tracks to the queue.", user.Name, numAdded)
|
||||||
|
if numTooLong != 0 {
|
||||||
|
retString += fmt.Sprintf("<br><b>%d</b> tracks could not be added due to error or because they are too long.", numTooLong)
|
||||||
|
}
|
||||||
|
return retString, false, nil
|
||||||
|
}
|
83
commands/add_test.go
Normal file
83
commands/add_test.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/add_test.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumbleffmpeg"
|
||||||
|
"github.com/matthieugrieger/mumbledj/bot"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AddCommandTestSuite struct {
|
||||||
|
Command AddCommand
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *AddCommandTestSuite) SetupSuite() {
|
||||||
|
DJ = bot.NewMumbleDJ()
|
||||||
|
bot.DJ = DJ
|
||||||
|
|
||||||
|
// Trick the tests into thinking audio is already playing to avoid
|
||||||
|
// attempting to play tracks that don't exist.
|
||||||
|
DJ.AudioStream = new(gumbleffmpeg.Stream)
|
||||||
|
|
||||||
|
viper.Set("commands.add.aliases", []string{"add", "a"})
|
||||||
|
viper.Set("commands.add.description", "add")
|
||||||
|
viper.Set("commands.add.is_admin", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *AddCommandTestSuite) SetupTest() {
|
||||||
|
DJ.Queue = bot.NewQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *AddCommandTestSuite) TestAliases() {
|
||||||
|
suite.Equal([]string{"add", "a"}, suite.Command.Aliases())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *AddCommandTestSuite) TestDescription() {
|
||||||
|
suite.Equal("add", suite.Command.Description())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *AddCommandTestSuite) TestIsAdminCommand() {
|
||||||
|
suite.False(suite.Command.IsAdminCommand())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *AddCommandTestSuite) TestExecuteWithNoArgs() {
|
||||||
|
message, isPrivateMessage, err := suite.Command.Execute(nil)
|
||||||
|
|
||||||
|
suite.Equal("", message, "No message should be returned since an error occurred.")
|
||||||
|
suite.True(isPrivateMessage, "This should be a private message.")
|
||||||
|
suite.NotNil(err, "An error should be returned for attempting to add a track without providing a URL.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement this test.
|
||||||
|
func (suite *AddCommandTestSuite) TestExecuteWhenNoTracksFound() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement this test.
|
||||||
|
func (suite *AddCommandTestSuite) TestExecuteWhenTrackFound() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement this test.
|
||||||
|
func (suite *AddCommandTestSuite) TestExecuteWhenMultipleTracksFound() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement this test.
|
||||||
|
func (suite *AddCommandTestSuite) TestExecuteWithMultipleURLs() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddCommandTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(AddCommandTestSuite))
|
||||||
|
}
|
98
commands/addnext.go
Normal file
98
commands/addnext.go
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/addnext.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/matthieugrieger/mumbledj/interfaces"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddNextCommand is a command that adds an audio track associated with a supported
|
||||||
|
// URL to the queue as the next item.
|
||||||
|
type AddNextCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *AddNextCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.addnext.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *AddNextCommand) Description() string {
|
||||||
|
return viper.GetString("commands.addnext.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *AddNextCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.addnext.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *AddNextCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
var (
|
||||||
|
allTracks []interfaces.Track
|
||||||
|
tracks []interfaces.Track
|
||||||
|
service interfaces.Service
|
||||||
|
err error
|
||||||
|
lastTrackAdded interfaces.Track
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
return "", true, errors.New("A URL must be supplied with the addnext command")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, arg := range args {
|
||||||
|
if service, err = DJ.GetService(arg); err == nil {
|
||||||
|
tracks, err = service.GetTracks(arg, user)
|
||||||
|
if err == nil {
|
||||||
|
allTracks = append(allTracks, tracks...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(allTracks) == 0 {
|
||||||
|
return "", true, errors.New("No valid tracks were found with the provided URL(s)")
|
||||||
|
}
|
||||||
|
|
||||||
|
numTooLong := 0
|
||||||
|
numAdded := 0
|
||||||
|
// We must loop backwards here to preserve the track order when inserting tracks.
|
||||||
|
for i := len(allTracks) - 1; i >= 0; i-- {
|
||||||
|
if err = DJ.Queue.InsertTrack(1, allTracks[i]); err != nil {
|
||||||
|
numTooLong++
|
||||||
|
} else {
|
||||||
|
numAdded++
|
||||||
|
lastTrackAdded = allTracks[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if numAdded == 0 {
|
||||||
|
return "", true, errors.New("Your track(s) were either too long or an error occurred while processing them. No track(s) have been added.")
|
||||||
|
} else if numAdded == 1 {
|
||||||
|
return fmt.Sprintf("<b>%s</b> added <b>1</b> track to the queue:<br>\"%s\" from %s",
|
||||||
|
user.Name, lastTrackAdded.GetTitle(), lastTrackAdded.GetService()), false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
retString := fmt.Sprintf("<b>%s</b> added <b>%d</b> tracks to the queue.", user.Name, numAdded)
|
||||||
|
if numTooLong != 0 {
|
||||||
|
retString += fmt.Sprintf("<br><b>%d</b> tracks could not be added due to error or because they are too long.", numTooLong)
|
||||||
|
}
|
||||||
|
return retString, false, nil
|
||||||
|
}
|
8
commands/addnext_test.go
Normal file
8
commands/addnext_test.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
* commands/addnext_test.go
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
55
commands/cachesize.go
Normal file
55
commands/cachesize.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/cachesize.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CacheSizeCommand is a command that outputs the current size of the cache.
|
||||||
|
type CacheSizeCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *CacheSizeCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.cachesize.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *CacheSizeCommand) Description() string {
|
||||||
|
return viper.GetString("commands.cachesize.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *CacheSizeCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.cachesize.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *CacheSizeCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
const bytesInMiB = 1048576
|
||||||
|
|
||||||
|
if !viper.GetBool("cache.enabled") {
|
||||||
|
return "", true, errors.New("Caching is currently disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
DJ.Cache.UpdateStatistics()
|
||||||
|
return fmt.Sprintf("The current size of the cache is <b>%.2v MiB</b>.", DJ.Cache.TotalFileSize/bytesInMiB), true, nil
|
||||||
|
}
|
60
commands/cachesize_test.go
Normal file
60
commands/cachesize_test.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/cachesize_test.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/matthieugrieger/mumbledj/bot"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CacheSizeCommandTestSuite struct {
|
||||||
|
Command CacheSizeCommand
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CacheSizeCommandTestSuite) SetupSuite() {
|
||||||
|
DJ = bot.NewMumbleDJ()
|
||||||
|
|
||||||
|
viper.Set("commands.cachesize.aliases", []string{"cachesize", "cs"})
|
||||||
|
viper.Set("commands.cachesize.description", "cachesize")
|
||||||
|
viper.Set("commands.cachesize.is_admin", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CacheSizeCommandTestSuite) TestAliases() {
|
||||||
|
suite.Equal([]string{"cachesize", "cs"}, suite.Command.Aliases())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CacheSizeCommandTestSuite) TestDescription() {
|
||||||
|
suite.Equal("cachesize", suite.Command.Description())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CacheSizeCommandTestSuite) TestIsAdminCommand() {
|
||||||
|
suite.True(suite.Command.IsAdminCommand())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CacheSizeCommandTestSuite) TestExecuteWhenCachingIsDisabled() {
|
||||||
|
viper.Set("cache.enabled", false)
|
||||||
|
|
||||||
|
message, isPrivateMessage, err := suite.Command.Execute(nil)
|
||||||
|
|
||||||
|
suite.Equal("", message, "An error occurred so no message should be returned.")
|
||||||
|
suite.True(isPrivateMessage, "This should be a private message.")
|
||||||
|
suite.NotNil(err, "An error should be returned because caching is disabled.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement this test.
|
||||||
|
func (suite *CacheSizeCommandTestSuite) TestExecuteWhenCachingIsEnabled() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCacheSizeCommandTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(CacheSizeCommandTestSuite))
|
||||||
|
}
|
60
commands/currenttrack.go
Normal file
60
commands/currenttrack.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/currenttrack.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/matthieugrieger/mumbledj/interfaces"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CurrentTrackCommand is a command that outputs information related to
|
||||||
|
// the track that is currently playing (if one exists).
|
||||||
|
type CurrentTrackCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *CurrentTrackCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.currenttrack.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *CurrentTrackCommand) Description() string {
|
||||||
|
return viper.GetString("commands.currenttrack.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *CurrentTrackCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.currenttrack.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *CurrentTrackCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
var (
|
||||||
|
currentTrack interfaces.Track
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if currentTrack, err = DJ.Queue.CurrentTrack(); err != nil {
|
||||||
|
return "", true, errors.New("There are no tracks in the queue")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("The current track is \"%s\", added by <b>%s</b>.",
|
||||||
|
currentTrack.GetTitle(), currentTrack.GetSubmitter()), true, nil
|
||||||
|
}
|
70
commands/currenttrack_test.go
Normal file
70
commands/currenttrack_test.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/currenttrack_test.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/layeh/gumble/gumbleffmpeg"
|
||||||
|
"github.com/matthieugrieger/mumbledj/bot"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CurrentTrackCommandTestSuite struct {
|
||||||
|
Command CurrentTrackCommand
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CurrentTrackCommandTestSuite) SetupSuite() {
|
||||||
|
DJ = bot.NewMumbleDJ()
|
||||||
|
|
||||||
|
// Trick the tests into thinking audio is already playing to avoid
|
||||||
|
// attempting to play tracks that don't exist.
|
||||||
|
DJ.AudioStream = new(gumbleffmpeg.Stream)
|
||||||
|
|
||||||
|
viper.Set("commands.currenttrack.aliases", []string{"currenttrack", "current"})
|
||||||
|
viper.Set("commands.currenttrack.description", "currenttrack")
|
||||||
|
viper.Set("commands.currenttrack.is_admin", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CurrentTrackCommandTestSuite) SetupTest() {
|
||||||
|
DJ.Queue = bot.NewQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CurrentTrackCommandTestSuite) TestAliases() {
|
||||||
|
suite.Equal([]string{"currenttrack", "current"}, suite.Command.Aliases())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CurrentTrackCommandTestSuite) TestDescription() {
|
||||||
|
suite.Equal("currenttrack", suite.Command.Description())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CurrentTrackCommandTestSuite) TestIsAdminCommand() {
|
||||||
|
suite.False(suite.Command.IsAdminCommand())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CurrentTrackCommandTestSuite) TestExecuteWhenQueueIsEmpty() {
|
||||||
|
message, isPrivateMessage, err := suite.Command.Execute(nil)
|
||||||
|
|
||||||
|
suite.Equal("", message, "No message should be returned since an error occurred.")
|
||||||
|
suite.True(isPrivateMessage, "This should be a private message.")
|
||||||
|
suite.NotNil(err, "An error should be returned since the queue is empty.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *CurrentTrackCommandTestSuite) TestExecuteWhenQueueNotEmpty() {
|
||||||
|
track := new(bot.Track)
|
||||||
|
track.Submitter = "test"
|
||||||
|
track.Title = "test"
|
||||||
|
|
||||||
|
DJ.Queue.AppendTrack(track)
|
||||||
|
|
||||||
|
message, isPrivateMessage, err := suite.Command.Execute(nil)
|
||||||
|
|
||||||
|
suite.NotEqual("", message, "A message should be returned with the current track information.")
|
||||||
|
suite.True(isPrivateMessage, "This should be a private message.")
|
||||||
|
suite.Nil(err, "No error should be returned.")
|
||||||
|
}
|
55
commands/forceskip.go
Normal file
55
commands/forceskip.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/forceskip.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ForceSkipCommand is a command that immediately skips the current track.
|
||||||
|
type ForceSkipCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *ForceSkipCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.forceskip.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *ForceSkipCommand) Description() string {
|
||||||
|
return viper.GetString("commands.forceskip.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *ForceSkipCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.forceskip.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *ForceSkipCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
if DJ.Queue.Length() == 0 {
|
||||||
|
return "", true, errors.New("The queue is currently empty. There are no tracks to skip")
|
||||||
|
}
|
||||||
|
|
||||||
|
DJ.Queue.Skip()
|
||||||
|
|
||||||
|
return fmt.Sprintf("The current track has been forcibly skipped by <b>%s</b>.",
|
||||||
|
user.Name), false, nil
|
||||||
|
}
|
8
commands/forceskip_test.go
Normal file
8
commands/forceskip_test.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
* commands/forceskip_test.go
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
66
commands/forceskipplaylist.go
Normal file
66
commands/forceskipplaylist.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/forceskipplaylist.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/matthieugrieger/mumbledj/interfaces"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ForceSkipPlaylistCommand is a command that immediately skips the current
|
||||||
|
// playlist.
|
||||||
|
type ForceSkipPlaylistCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *ForceSkipPlaylistCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.forceskipplaylist.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *ForceSkipPlaylistCommand) Description() string {
|
||||||
|
return viper.GetString("commands.forceskipplaylist.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *ForceSkipPlaylistCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.forceskipplaylist.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *ForceSkipPlaylistCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
var (
|
||||||
|
currentTrack interfaces.Track
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if currentTrack, err = DJ.Queue.CurrentTrack(); err != nil {
|
||||||
|
return "", true, errors.New("The queue is currently empty. There are no playlists to skip")
|
||||||
|
}
|
||||||
|
|
||||||
|
if playlist := currentTrack.GetPlaylist(); playlist == nil {
|
||||||
|
return "", true, errors.New("The current track is not part of a playlist")
|
||||||
|
}
|
||||||
|
|
||||||
|
DJ.Queue.SkipPlaylist()
|
||||||
|
|
||||||
|
return fmt.Sprintf("The current playlist has been forcibly skipped by <b>%s</b>.",
|
||||||
|
user.Name), false, nil
|
||||||
|
}
|
8
commands/forceskipplaylist_test.go
Normal file
8
commands/forceskipplaylist_test.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
* commands/forceskipplaylist_test.go
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
75
commands/help.go
Normal file
75
commands/help.go
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/help.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HelpCommand is a command that outputs a help message that shows the
|
||||||
|
// available commands and their aliases.
|
||||||
|
type HelpCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *HelpCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.help.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *HelpCommand) Description() string {
|
||||||
|
return viper.GetString("commands.help.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *HelpCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.help.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *HelpCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
commandString := "<b>%s</b> -- %s<br>"
|
||||||
|
regularCommands := ""
|
||||||
|
adminCommands := ""
|
||||||
|
totalString := ""
|
||||||
|
|
||||||
|
for _, command := range Commands {
|
||||||
|
currentString := fmt.Sprintf(commandString, command.Aliases(), command.Description())
|
||||||
|
if command.IsAdminCommand() {
|
||||||
|
adminCommands += currentString
|
||||||
|
} else {
|
||||||
|
regularCommands += currentString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalString = "<br><b>Commands:</b><br>" + regularCommands
|
||||||
|
|
||||||
|
isAdmin := false
|
||||||
|
if viper.GetBool("admins.enabled") {
|
||||||
|
isAdmin = DJ.IsAdmin(user)
|
||||||
|
} else {
|
||||||
|
isAdmin = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if isAdmin {
|
||||||
|
totalString += "<br><b>Admin Commands:</b><br>" + adminCommands
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalString, true, nil
|
||||||
|
}
|
72
commands/help_test.go
Normal file
72
commands/help_test.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/help_test.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/matthieugrieger/mumbledj/bot"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HelpCommandTestSuite struct {
|
||||||
|
Command HelpCommand
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *HelpCommandTestSuite) SetupSuite() {
|
||||||
|
DJ = bot.NewMumbleDJ()
|
||||||
|
|
||||||
|
viper.Set("commands.help.aliases", []string{"help", "h"})
|
||||||
|
viper.Set("commands.help.description", "help")
|
||||||
|
viper.Set("commands.help.is_admin", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *HelpCommandTestSuite) SetupTest() {
|
||||||
|
viper.Set("admins.enabled", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *HelpCommandTestSuite) TestAliases() {
|
||||||
|
suite.Equal([]string{"help", "h"}, suite.Command.Aliases())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *HelpCommandTestSuite) TestDescription() {
|
||||||
|
suite.Equal("help", suite.Command.Description())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *HelpCommandTestSuite) TestIsAdminCommand() {
|
||||||
|
suite.False(suite.Command.IsAdminCommand())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement this test.
|
||||||
|
func (suite *HelpCommandTestSuite) TestExecuteWhenPermissionsEnabledAndUserIsNotAdmin() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement this test.
|
||||||
|
func (suite *HelpCommandTestSuite) TestExecuteWhenPermissionsEnabledAndUserIsAdmin() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *HelpCommandTestSuite) TestExecuteWhenPermissionsDisabled() {
|
||||||
|
viper.Set("admins.enabled", false)
|
||||||
|
|
||||||
|
message, isPrivateMessage, err := suite.Command.Execute(nil)
|
||||||
|
|
||||||
|
suite.NotEqual("", message, "A message should be returned.")
|
||||||
|
suite.True(isPrivateMessage, "This should be a private message.")
|
||||||
|
suite.Nil(err, "No error should be returned.")
|
||||||
|
suite.Contains(message, "help", "The returned message should contain command descriptions.")
|
||||||
|
suite.Contains(message, "add", "The returned message should contain command descriptions.")
|
||||||
|
suite.Contains(message, "Admin Commands", "The returned message should contain admin command descriptions.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHelpCommandTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(HelpCommandTestSuite))
|
||||||
|
}
|
58
commands/joinme.go
Normal file
58
commands/joinme.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/joinme.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/layeh/gumble/gumbleffmpeg"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JoinMeCommand is a command that moves the bot to the channel of the user
|
||||||
|
// who issued the command.
|
||||||
|
type JoinMeCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *JoinMeCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.joinme.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *JoinMeCommand) Description() string {
|
||||||
|
return viper.GetString("commands.joinme.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *JoinMeCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.joinme.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *JoinMeCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
if DJ.AudioStream != nil && DJ.AudioStream.State() == gumbleffmpeg.StatePlaying &&
|
||||||
|
len(DJ.Client.Self.Channel.Users) > 1 {
|
||||||
|
return "", true, errors.New("Users in another channel are listening to me.")
|
||||||
|
}
|
||||||
|
|
||||||
|
DJ.Client.Do(func() {
|
||||||
|
DJ.Client.Self.Move(user.Channel)
|
||||||
|
})
|
||||||
|
|
||||||
|
return "I am now in your channel!", true, nil
|
||||||
|
}
|
8
commands/joinme_test.go
Normal file
8
commands/joinme_test.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
* commands/joinme_test.go
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
55
commands/kill.go
Normal file
55
commands/kill.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/currenttrack.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KillCommand is a command that safely kills the bot.
|
||||||
|
type KillCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *KillCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.kill.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *KillCommand) Description() string {
|
||||||
|
return viper.GetString("commands.kill.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *KillCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.kill.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *KillCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
if err := DJ.Cache.DeleteAll(); err != nil {
|
||||||
|
return "", true, err
|
||||||
|
}
|
||||||
|
if err := DJ.Client.Disconnect(); err != nil {
|
||||||
|
return "", true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(0)
|
||||||
|
return "", true, nil
|
||||||
|
}
|
8
commands/kill_test.go
Normal file
8
commands/kill_test.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
* commands/kill_test.go
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
73
commands/listtracks.go
Normal file
73
commands/listtracks.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/currenttrack.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/matthieugrieger/mumbledj/interfaces"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListTracksCommand is a command that lists the tracks that are currently
|
||||||
|
// in the queue.
|
||||||
|
type ListTracksCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *ListTracksCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.listtracks.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *ListTracksCommand) Description() string {
|
||||||
|
return viper.GetString("commands.listtracks.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *ListTracksCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.listtracks.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *ListTracksCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
if DJ.Queue.Length() == 0 {
|
||||||
|
return "", true, errors.New("There are no tracks currently in the queue")
|
||||||
|
}
|
||||||
|
|
||||||
|
numTracksToList := DJ.Queue.Length()
|
||||||
|
if len(args) != 0 {
|
||||||
|
if parsedNum, err := strconv.Atoi(args[0]); err == nil {
|
||||||
|
numTracksToList = parsedNum
|
||||||
|
} else {
|
||||||
|
return "", true, errors.New("An invalid integer was supplied")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
DJ.Queue.Traverse(func(i int, track interfaces.Track) {
|
||||||
|
if i < numTracksToList {
|
||||||
|
buffer.WriteString(fmt.Sprintf("<b>%d</b>: \"%s\", added by <b>%s</b>.<br>",
|
||||||
|
i+1, track.GetTitle(), track.GetSubmitter()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return buffer.String(), true, nil
|
||||||
|
}
|
136
commands/listtracks_test.go
Normal file
136
commands/listtracks_test.go
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/listtracks_test.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumbleffmpeg"
|
||||||
|
"github.com/matthieugrieger/mumbledj/bot"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ListTracksCommandTestSuite struct {
|
||||||
|
Command ListTracksCommand
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ListTracksCommandTestSuite) SetupSuite() {
|
||||||
|
DJ = bot.NewMumbleDJ()
|
||||||
|
bot.DJ = DJ
|
||||||
|
|
||||||
|
// Trick the tests into thinking audio is already playing to avoid
|
||||||
|
// attempting to play tracks that don't exist.
|
||||||
|
DJ.AudioStream = new(gumbleffmpeg.Stream)
|
||||||
|
|
||||||
|
viper.Set("commands.listtracks.aliases", []string{"listtracks", "list"})
|
||||||
|
viper.Set("commands.listtracks.description", "listtracks")
|
||||||
|
viper.Set("commands.listtracks.is_admin", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ListTracksCommandTestSuite) SetupTest() {
|
||||||
|
DJ.Queue = bot.NewQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ListTracksCommandTestSuite) TestAliases() {
|
||||||
|
suite.Equal([]string{"listtracks", "list"}, suite.Command.Aliases())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ListTracksCommandTestSuite) TestDescription() {
|
||||||
|
suite.Equal("listtracks", suite.Command.Description())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ListTracksCommandTestSuite) TestIsAdminCommand() {
|
||||||
|
suite.False(suite.Command.IsAdminCommand())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ListTracksCommandTestSuite) TestExecuteWithNoTracks() {
|
||||||
|
message, isPrivateMessage, err := suite.Command.Execute(nil)
|
||||||
|
|
||||||
|
suite.Equal("", message, "No message should be returned.")
|
||||||
|
suite.True(isPrivateMessage, "This should be a private message.")
|
||||||
|
suite.NotNil(err, "An error should be returned as there are no tracks to list.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ListTracksCommandTestSuite) TestExecuteWithNoArg() {
|
||||||
|
track := new(bot.Track)
|
||||||
|
track.Title = "title"
|
||||||
|
track.Submitter = "test"
|
||||||
|
|
||||||
|
DJ.Queue.AppendTrack(track)
|
||||||
|
|
||||||
|
message, isPrivateMessage, err := suite.Command.Execute(nil)
|
||||||
|
|
||||||
|
suite.NotEqual("", message, "A message containing track information should be returned.")
|
||||||
|
suite.Contains(message, "title", "The returned message should contain the track title.")
|
||||||
|
suite.Contains(message, "test", "The returned message should contain the track submitter.")
|
||||||
|
suite.True(isPrivateMessage, "This should be a private message.")
|
||||||
|
suite.Nil(err, "No error should be returned.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ListTracksCommandTestSuite) TestExecuteWithValidArg() {
|
||||||
|
track1 := new(bot.Track)
|
||||||
|
track1.Title = "first"
|
||||||
|
track1.Submitter = "test"
|
||||||
|
|
||||||
|
track2 := new(bot.Track)
|
||||||
|
track2.Title = "second"
|
||||||
|
track2.Submitter = "test"
|
||||||
|
|
||||||
|
track3 := new(bot.Track)
|
||||||
|
track3.Title = "third"
|
||||||
|
track3.Submitter = "test"
|
||||||
|
|
||||||
|
DJ.Queue.AppendTrack(track1)
|
||||||
|
DJ.Queue.AppendTrack(track2)
|
||||||
|
DJ.Queue.AppendTrack(track3)
|
||||||
|
|
||||||
|
message, isPrivateMessage, err := suite.Command.Execute(nil, "2")
|
||||||
|
|
||||||
|
suite.NotEqual("", message, "A message containing track information should be returned.")
|
||||||
|
suite.Contains(message, "first", "The returned message should contain the first track.")
|
||||||
|
suite.Contains(message, "second", "The returned message should contain the second track.")
|
||||||
|
suite.NotContains(message, "third", "The returned message should not contain the third track.")
|
||||||
|
suite.True(isPrivateMessage, "This should be a private message.")
|
||||||
|
suite.Nil(err, "No error should be returned.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ListTracksCommandTestSuite) TestExecuteWithArgLargerThanQueueLength() {
|
||||||
|
track := new(bot.Track)
|
||||||
|
track.Title = "track"
|
||||||
|
track.Submitter = "test"
|
||||||
|
|
||||||
|
DJ.Queue.AppendTrack(track)
|
||||||
|
|
||||||
|
message, isPrivateMessage, err := suite.Command.Execute(nil, "2")
|
||||||
|
|
||||||
|
suite.NotEqual("", message, "A message containing track information should be returned.")
|
||||||
|
suite.Contains(message, "1", "The returned message should contain the first track.")
|
||||||
|
suite.NotContains(message, "2", "The returned message should not contain any further tracks.")
|
||||||
|
suite.True(isPrivateMessage, "This should be a private message.")
|
||||||
|
suite.Nil(err, "No error should be returned.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ListTracksCommandTestSuite) TestExecuteWithInvalidArg() {
|
||||||
|
track := new(bot.Track)
|
||||||
|
track.Title = "track"
|
||||||
|
track.Submitter = "test"
|
||||||
|
|
||||||
|
DJ.Queue.AppendTrack(track)
|
||||||
|
|
||||||
|
message, isPrivateMessage, err := suite.Command.Execute(nil, "test")
|
||||||
|
|
||||||
|
suite.Equal("", message, "No message should be returned.")
|
||||||
|
suite.True(isPrivateMessage, "This should be a private message.")
|
||||||
|
suite.NotNil(err, "An error should be returned due to an invalid argument being supplied.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListTracksCommandTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(ListTracksCommandTestSuite))
|
||||||
|
}
|
65
commands/move.go
Normal file
65
commands/move.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/move.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MoveCommand is a command that moves the bot from one channel to another.
|
||||||
|
type MoveCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *MoveCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.move.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *MoveCommand) Description() string {
|
||||||
|
return viper.GetString("commands.move.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *MoveCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.move.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *MoveCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return "", true, errors.New("A destination channel must be supplied to move the bot")
|
||||||
|
}
|
||||||
|
channel := ""
|
||||||
|
for _, arg := range args {
|
||||||
|
channel += arg + " "
|
||||||
|
}
|
||||||
|
channel = strings.TrimSpace(channel)
|
||||||
|
if channels := strings.Split(channel, "/"); DJ.Client.Channels.Find(channels...) != nil {
|
||||||
|
DJ.Client.Do(func() {
|
||||||
|
DJ.Client.Self.Move(DJ.Client.Channels.Find(channels...))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return "", true, errors.New("The provided channel does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("You have successfully moved the bot to <b>%s</b>.", channel), true, nil
|
||||||
|
}
|
8
commands/move_test.go
Normal file
8
commands/move_test.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
* commands/move_test.go
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
60
commands/nexttrack.go
Normal file
60
commands/nexttrack.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/nexttrack.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NextTrackCommand is a command that outputs information related to the next
|
||||||
|
// track in the queue (if one exists).
|
||||||
|
type NextTrackCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *NextTrackCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.nexttrack.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *NextTrackCommand) Description() string {
|
||||||
|
return viper.GetString("commands.nexttrack.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *NextTrackCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.nexttrack.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *NextTrackCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
length := DJ.Queue.Length()
|
||||||
|
if length == 0 {
|
||||||
|
return "", true, errors.New("There are no tracks in the queue")
|
||||||
|
}
|
||||||
|
if length == 1 {
|
||||||
|
return "", true, errors.New("The current track is the only track in the queue")
|
||||||
|
}
|
||||||
|
|
||||||
|
nextTrack, _ := DJ.Queue.PeekNextTrack()
|
||||||
|
|
||||||
|
return fmt.Sprintf("The next track is \"%s\", added by <b>%s</b>.",
|
||||||
|
nextTrack.GetTitle(), nextTrack.GetSubmitter()), true, nil
|
||||||
|
}
|
97
commands/nexttrack_test.go
Normal file
97
commands/nexttrack_test.go
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/nexttrack_test.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumbleffmpeg"
|
||||||
|
"github.com/matthieugrieger/mumbledj/bot"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NextTrackCommandTestSuite struct {
|
||||||
|
Command NextTrackCommand
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NextTrackCommandTestSuite) SetupSuite() {
|
||||||
|
DJ = bot.NewMumbleDJ()
|
||||||
|
bot.DJ = DJ
|
||||||
|
|
||||||
|
// Trick the tests into thinking audio is already playing to avoid
|
||||||
|
// attempting to play tracks that don't exist.
|
||||||
|
DJ.AudioStream = new(gumbleffmpeg.Stream)
|
||||||
|
|
||||||
|
viper.Set("commands.nexttrack.aliases", []string{"nexttrack", "next"})
|
||||||
|
viper.Set("commands.nexttrack.description", "nexttrack")
|
||||||
|
viper.Set("commands.nexttrack.is_admin", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NextTrackCommandTestSuite) SetupTest() {
|
||||||
|
DJ.Queue = bot.NewQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NextTrackCommandTestSuite) TestAliases() {
|
||||||
|
suite.Equal([]string{"nexttrack", "next"}, suite.Command.Aliases())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NextTrackCommandTestSuite) TestDescription() {
|
||||||
|
suite.Equal("nexttrack", suite.Command.Description())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NextTrackCommandTestSuite) TestIsAdminCommand() {
|
||||||
|
suite.False(suite.Command.IsAdminCommand())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NextTrackCommandTestSuite) TestExecuteWhenQueueIsEmpty() {
|
||||||
|
message, isPrivateMessage, err := suite.Command.Execute(nil)
|
||||||
|
|
||||||
|
suite.Equal("", message, "No message should be returned.")
|
||||||
|
suite.True(isPrivateMessage, "This should be a private message.")
|
||||||
|
suite.NotNil(err, "An error should be returned due to the queue being empty.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NextTrackCommandTestSuite) TestExecuteWhenQueueHasOneTrack() {
|
||||||
|
track := new(bot.Track)
|
||||||
|
track.Title = "test"
|
||||||
|
track.Submitter = "test"
|
||||||
|
|
||||||
|
DJ.Queue.AppendTrack(track)
|
||||||
|
|
||||||
|
message, isPrivateMessage, err := suite.Command.Execute(nil)
|
||||||
|
|
||||||
|
suite.Equal("", message, "No message should be returned.")
|
||||||
|
suite.True(isPrivateMessage, "This should be a private message.")
|
||||||
|
suite.NotNil(err, "An error should be returned due to the queue having only one track.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NextTrackCommandTestSuite) TestExecuteWhenQueueHasTwoOrMoreTracks() {
|
||||||
|
track1 := new(bot.Track)
|
||||||
|
track1.Title = "first"
|
||||||
|
track1.Submitter = "test"
|
||||||
|
|
||||||
|
track2 := new(bot.Track)
|
||||||
|
track2.Title = "second"
|
||||||
|
track2.Submitter = "test"
|
||||||
|
|
||||||
|
DJ.Queue.AppendTrack(track1)
|
||||||
|
DJ.Queue.AppendTrack(track2)
|
||||||
|
|
||||||
|
message, isPrivateMessage, err := suite.Command.Execute(nil)
|
||||||
|
|
||||||
|
suite.NotEqual("", "A message containing information for the next track should be returned.")
|
||||||
|
suite.Contains(message, "second", "The returned message should contain information about the second track in the queue.")
|
||||||
|
suite.True(isPrivateMessage, "This should be a private message.")
|
||||||
|
suite.Nil(err, "No error should be returned.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextTrackCommandTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(NextTrackCommandTestSuite))
|
||||||
|
}
|
55
commands/numcached.go
Normal file
55
commands/numcached.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/numcached.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NumCachedCommand is a command that outputs the number of tracks that
|
||||||
|
// are currently cached on disk (if caching is enabled).
|
||||||
|
type NumCachedCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *NumCachedCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.numcached.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *NumCachedCommand) Description() string {
|
||||||
|
return viper.GetString("commands.numcached.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *NumCachedCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.numcached.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *NumCachedCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
if !viper.GetBool("cache.enabled") {
|
||||||
|
return "", true, errors.New("Caching is currently disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
DJ.Cache.UpdateStatistics()
|
||||||
|
return fmt.Sprintf("There are currently <b>%d</b> items stored in the cache.",
|
||||||
|
DJ.Cache.NumAudioFiles), true, nil
|
||||||
|
}
|
8
commands/numcached_test.go
Normal file
8
commands/numcached_test.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
* commands/numcached_test.go
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
53
commands/numtracks.go
Normal file
53
commands/numtracks.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/numtracks.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NumTracksCommand is a command that outputs the current number of tracks
|
||||||
|
// in the queue.
|
||||||
|
type NumTracksCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *NumTracksCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.numtracks.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *NumTracksCommand) Description() string {
|
||||||
|
return viper.GetString("commands.numtracks.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *NumTracksCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.numtracks.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *NumTracksCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
length := DJ.Queue.Length()
|
||||||
|
if length == 1 {
|
||||||
|
return "There is currently <b>1</b> track in the queue.", true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("There are currently <b>%d</b> tracks in the queue.", length), true, nil
|
||||||
|
}
|
99
commands/numtracks_test.go
Normal file
99
commands/numtracks_test.go
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/numtracks_test.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumbleffmpeg"
|
||||||
|
"github.com/matthieugrieger/mumbledj/bot"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NumTracksCommandTestSuite struct {
|
||||||
|
Command NumTracksCommand
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NumTracksCommandTestSuite) SetupSuite() {
|
||||||
|
DJ = bot.NewMumbleDJ()
|
||||||
|
bot.DJ = DJ
|
||||||
|
|
||||||
|
// Trick the tests into thinking audio is already playing to avoid
|
||||||
|
// attempting to play tracks that don't exist.
|
||||||
|
DJ.AudioStream = new(gumbleffmpeg.Stream)
|
||||||
|
|
||||||
|
viper.Set("commands.numtracks.aliases", []string{"numtracks", "num"})
|
||||||
|
viper.Set("commands.numtracks.description", "numtracks")
|
||||||
|
viper.Set("commands.numtracks.is_admin", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NumTracksCommandTestSuite) SetupTest() {
|
||||||
|
DJ.Queue = bot.NewQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NumTracksCommandTestSuite) TestAliases() {
|
||||||
|
suite.Equal([]string{"numtracks", "num"}, suite.Command.Aliases())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NumTracksCommandTestSuite) TestDescription() {
|
||||||
|
suite.Equal("numtracks", suite.Command.Description())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NumTracksCommandTestSuite) TestIsAdminCommand() {
|
||||||
|
suite.False(suite.Command.IsAdminCommand())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NumTracksCommandTestSuite) TestExecuteWhenZeroTracksAreInQueue() {
|
||||||
|
message, isPrivateMessage, err := suite.Command.Execute(nil)
|
||||||
|
|
||||||
|
suite.NotEqual("", message, "A message should be returned.")
|
||||||
|
suite.Contains(message, "<b>0</b> tracks", "The returned message should state that there are no tracks in the queue.")
|
||||||
|
suite.True(isPrivateMessage, "This should be a private message.")
|
||||||
|
suite.Nil(err, "No error should be returned.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NumTracksCommandTestSuite) TestExecuteWhenOneTrackIsInQueue() {
|
||||||
|
track := new(bot.Track)
|
||||||
|
track.Title = "test"
|
||||||
|
track.Submitter = "test"
|
||||||
|
|
||||||
|
DJ.Queue.AppendTrack(track)
|
||||||
|
|
||||||
|
message, isPrivateMessage, err := suite.Command.Execute(nil)
|
||||||
|
|
||||||
|
suite.NotEqual("", message, "A message should be returned.")
|
||||||
|
suite.Contains(message, "<b>1</b> track", "The returned message should state that there is one track in the queue.")
|
||||||
|
suite.True(isPrivateMessage, "This should be a private message.")
|
||||||
|
suite.Nil(err, "No error should be returned.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *NumTracksCommandTestSuite) TestExecuteWhenTwoOrMoreTracksAreInQueue() {
|
||||||
|
track1 := new(bot.Track)
|
||||||
|
track1.Title = "test"
|
||||||
|
track1.Submitter = "test"
|
||||||
|
|
||||||
|
track2 := new(bot.Track)
|
||||||
|
track2.Title = "test"
|
||||||
|
track2.Submitter = "test"
|
||||||
|
|
||||||
|
DJ.Queue.AppendTrack(track1)
|
||||||
|
DJ.Queue.AppendTrack(track2)
|
||||||
|
|
||||||
|
message, isPrivateMessage, err := suite.Command.Execute(nil)
|
||||||
|
|
||||||
|
suite.NotEqual("", "A message should be returned.")
|
||||||
|
suite.Contains(message, "tracks", "The returned message should use the plural form of the word track.")
|
||||||
|
suite.True(isPrivateMessage, "This should be a private message.")
|
||||||
|
suite.Nil(err, "No error should be returned.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNumTracksCommandTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(NumTracksCommandTestSuite))
|
||||||
|
}
|
51
commands/pause.go
Normal file
51
commands/pause.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/pause.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PauseCommand is a command that pauses audio playback.
|
||||||
|
type PauseCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *PauseCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.pause.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *PauseCommand) Description() string {
|
||||||
|
return viper.GetString("commands.pause.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *PauseCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.pause.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *PauseCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
err := DJ.Queue.PauseCurrent()
|
||||||
|
if err != nil {
|
||||||
|
return "", true, err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("<b>%s</b> has paused audio playback.", user.Name), false, nil
|
||||||
|
}
|
8
commands/pause_test.go
Normal file
8
commands/pause_test.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
* commands/pause_test.go
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
49
commands/pkg_init.go
Normal file
49
commands/pkg_init.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/pkg_init.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/matthieugrieger/mumbledj/bot"
|
||||||
|
"github.com/matthieugrieger/mumbledj/interfaces"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DJ is an injected MumbleDJ struct.
|
||||||
|
var DJ *bot.MumbleDJ
|
||||||
|
|
||||||
|
// Commands is a slice of all enabled commands.
|
||||||
|
var Commands []interfaces.Command
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Commands = []interfaces.Command{
|
||||||
|
new(AddCommand),
|
||||||
|
new(AddNextCommand),
|
||||||
|
new(CacheSizeCommand),
|
||||||
|
new(CurrentTrackCommand),
|
||||||
|
new(ForceSkipCommand),
|
||||||
|
new(ForceSkipPlaylistCommand),
|
||||||
|
new(HelpCommand),
|
||||||
|
new(JoinMeCommand),
|
||||||
|
new(KillCommand),
|
||||||
|
new(ListTracksCommand),
|
||||||
|
new(MoveCommand),
|
||||||
|
new(NextTrackCommand),
|
||||||
|
new(NumCachedCommand),
|
||||||
|
new(NumTracksCommand),
|
||||||
|
new(PauseCommand),
|
||||||
|
new(ReloadCommand),
|
||||||
|
new(ResetCommand),
|
||||||
|
new(ResumeCommand),
|
||||||
|
new(SetCommentCommand),
|
||||||
|
new(ShuffleCommand),
|
||||||
|
new(SkipCommand),
|
||||||
|
new(SkipPlaylistCommand),
|
||||||
|
new(ToggleShuffleCommand),
|
||||||
|
new(VersionCommand),
|
||||||
|
new(VolumeCommand),
|
||||||
|
}
|
||||||
|
}
|
52
commands/reload.go
Normal file
52
commands/reload.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/reload.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/matthieugrieger/mumbledj/bot"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReloadCommand is a command that reloads the configuration values for the bot
|
||||||
|
// from a config file.
|
||||||
|
type ReloadCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *ReloadCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.reload.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *ReloadCommand) Description() string {
|
||||||
|
return viper.GetString("commands.reload.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *ReloadCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.reload.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *ReloadCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
if err := bot.ReadConfigFile(); err != nil {
|
||||||
|
return "", true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return "The configuration in the configuration file has been reloaded successfully.",
|
||||||
|
true, nil
|
||||||
|
}
|
8
commands/reload_test.go
Normal file
8
commands/reload_test.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
* commands/reload_test.go
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
63
commands/reset.go
Normal file
63
commands/reset.go
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/reset.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResetCommand is a command that resets the queue and cache.
|
||||||
|
type ResetCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *ResetCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.reset.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *ResetCommand) Description() string {
|
||||||
|
return viper.GetString("commands.reset.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *ResetCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.reset.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *ResetCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
if DJ.Queue.Length() == 0 {
|
||||||
|
return "", true, errors.New("The queue is already empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if DJ.AudioStream != nil {
|
||||||
|
DJ.AudioStream.Stop()
|
||||||
|
DJ.AudioStream = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
DJ.Queue.Reset()
|
||||||
|
|
||||||
|
if err := DJ.Cache.DeleteAll(); err != nil {
|
||||||
|
return "", true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("<b>%s</b> has reset the queue.", user.Name), false, nil
|
||||||
|
}
|
8
commands/reset_test.go
Normal file
8
commands/reset_test.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
* commands/reset_test.go
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
51
commands/resume.go
Normal file
51
commands/resume.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/resume.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResumeCommand is a command that resumes audio playback.
|
||||||
|
type ResumeCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *ResumeCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.resume.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *ResumeCommand) Description() string {
|
||||||
|
return viper.GetString("commands.resume.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *ResumeCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.resume.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *ResumeCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
err := DJ.Queue.ResumeCurrent()
|
||||||
|
if err != nil {
|
||||||
|
return "", true, err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("<b>%s</b> has resumed audio playback.", user.Name), false, nil
|
||||||
|
}
|
8
commands/resume_test.go
Normal file
8
commands/resume_test.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
* commands/resume_test.go
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
66
commands/setcomment.go
Normal file
66
commands/setcomment.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/setcomment.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetCommentCommand is a command that changes the Mumble comment of the bot.
|
||||||
|
type SetCommentCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *SetCommentCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.setcomment.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *SetCommentCommand) Description() string {
|
||||||
|
return viper.GetString("commands.setcomment.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *SetCommentCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.setcomment.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *SetCommentCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
DJ.Client.Do(func() {
|
||||||
|
DJ.Client.Self.SetComment("")
|
||||||
|
})
|
||||||
|
return "The comment for the bot has been successfully removed.", true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var newComment string
|
||||||
|
for _, arg := range args {
|
||||||
|
newComment += arg + " "
|
||||||
|
}
|
||||||
|
strings.TrimSpace(newComment)
|
||||||
|
|
||||||
|
DJ.Client.Do(func() {
|
||||||
|
DJ.Client.Self.SetComment(newComment)
|
||||||
|
})
|
||||||
|
|
||||||
|
return fmt.Sprintf("The comment for the bot has been successfully changed to the following: %s",
|
||||||
|
newComment), true, nil
|
||||||
|
}
|
8
commands/setcomment_test.go
Normal file
8
commands/setcomment_test.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
* commands/setcomment_test.go
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
57
commands/shuffle.go
Normal file
57
commands/shuffle.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/shuffle.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ShuffleCommand is a command that shuffles the audio queue.
|
||||||
|
type ShuffleCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *ShuffleCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.shuffle.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *ShuffleCommand) Description() string {
|
||||||
|
return viper.GetString("commands.shuffle.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *ShuffleCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.shuffle.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *ShuffleCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
length := DJ.Queue.Length()
|
||||||
|
if length == 0 {
|
||||||
|
return "", true, errors.New("There are no tracks currently in the queue")
|
||||||
|
}
|
||||||
|
if length <= 2 {
|
||||||
|
return "", true, errors.New("There are not enough tracks in the queue to execute a shuffle")
|
||||||
|
}
|
||||||
|
|
||||||
|
DJ.Queue.ShuffleTracks()
|
||||||
|
|
||||||
|
return "The audio queue has been shuffled.", false, nil
|
||||||
|
}
|
8
commands/shuffle_test.go
Normal file
8
commands/shuffle_test.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
* commands/shuffle_test.go
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
55
commands/skip.go
Normal file
55
commands/skip.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/skip.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SkipCommand is a command that places a vote to skip the current track.
|
||||||
|
type SkipCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *SkipCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.skip.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *SkipCommand) Description() string {
|
||||||
|
return viper.GetString("commands.skip.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *SkipCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.skip.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *SkipCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
if DJ.Queue.Length() == 0 {
|
||||||
|
return "", true, errors.New("The queue is currently empty. There is no track to skip")
|
||||||
|
}
|
||||||
|
if err := DJ.Skips.AddTrackSkip(user); err != nil {
|
||||||
|
return "", true, errors.New("You have already voted to skip this track")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("<b>%s</b> has voted to skip the current track.", user.Name), false, nil
|
||||||
|
}
|
8
commands/skip_test.go
Normal file
8
commands/skip_test.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
* commands/skip_test.go
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
66
commands/skipplaylist.go
Normal file
66
commands/skipplaylist.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/skipplaylist.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/matthieugrieger/mumbledj/interfaces"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SkipPlaylistCommand is a command that places a vote to skip the current
|
||||||
|
// playlist.
|
||||||
|
type SkipPlaylistCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *SkipPlaylistCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.skipplaylist.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *SkipPlaylistCommand) Description() string {
|
||||||
|
return viper.GetString("commands.skipplaylist.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *SkipPlaylistCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.skipplaylist.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *SkipPlaylistCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
var (
|
||||||
|
currentTrack interfaces.Track
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if currentTrack, err = DJ.Queue.CurrentTrack(); err != nil {
|
||||||
|
return "", true, errors.New("The queue is currently empty. There is no playlist to skip")
|
||||||
|
}
|
||||||
|
|
||||||
|
if playlist := currentTrack.GetPlaylist(); playlist == nil {
|
||||||
|
return "", true, errors.New("The current track is not part of a playlist")
|
||||||
|
}
|
||||||
|
if err := DJ.Skips.AddPlaylistSkip(user); err != nil {
|
||||||
|
return "", true, errors.New("You have already voted to skip this playlist")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("<b>%s</b> has voted to skip the current playlist.", user.Name), false, nil
|
||||||
|
}
|
8
commands/skipplaylist_test.go
Normal file
8
commands/skipplaylist_test.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
* commands/skipplaylist_test.go
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
50
commands/toggleshuffle.go
Normal file
50
commands/toggleshuffle.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/toggleshuffle.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToggleShuffleCommand is a command that changes the Mumble comment of the bot.
|
||||||
|
type ToggleShuffleCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *ToggleShuffleCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.toggleshuffle.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *ToggleShuffleCommand) Description() string {
|
||||||
|
return viper.GetString("commands.toggleshuffle.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *ToggleShuffleCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.toggleshuffle.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *ToggleShuffleCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
if viper.GetBool("queue.automatic_shuffle_on") {
|
||||||
|
viper.Set("queue.automatic_shuffle_on", false)
|
||||||
|
return "Automatic shuffling has been toggled off.", false, nil
|
||||||
|
}
|
||||||
|
viper.Set("queue.automatic_shuffle_on", true)
|
||||||
|
return "Automatic shuffling has been toggled on.", false, nil
|
||||||
|
}
|
67
commands/toggleshuffle_test.go
Normal file
67
commands/toggleshuffle_test.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/toggleshuffle_test.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/matthieugrieger/mumbledj/bot"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ToggleShuffleCommandTestSuite struct {
|
||||||
|
Command ToggleShuffleCommand
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ToggleShuffleCommandTestSuite) SetupSuite() {
|
||||||
|
DJ = bot.NewMumbleDJ()
|
||||||
|
|
||||||
|
viper.Set("commands.toggleshuffle.aliases", []string{"toggleshuffle", "ts"})
|
||||||
|
viper.Set("commands.toggleshuffle.description", "toggleshuffle")
|
||||||
|
viper.Set("commands.toggleshuffle.is_admin", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ToggleShuffleCommandTestSuite) TestAliases() {
|
||||||
|
suite.Equal([]string{"toggleshuffle", "ts"}, suite.Command.Aliases())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ToggleShuffleCommandTestSuite) TestDescription() {
|
||||||
|
suite.Equal("toggleshuffle", suite.Command.Description())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ToggleShuffleCommandTestSuite) TestIsAdminCommand() {
|
||||||
|
suite.True(suite.Command.IsAdminCommand())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ToggleShuffleCommandTestSuite) TestExecuteWhenShuffleIsOff() {
|
||||||
|
viper.Set("queue.automatic_shuffle_on", false)
|
||||||
|
|
||||||
|
message, isPrivateMessage, err := suite.Command.Execute(nil)
|
||||||
|
|
||||||
|
suite.NotEqual("", message, "A message should be returned.")
|
||||||
|
suite.False(isPrivateMessage, "This should not be a private message.")
|
||||||
|
suite.Nil(err, "No error should be returned.")
|
||||||
|
suite.True(viper.GetBool("queue.automatic_shuffle_on"), "Automatic shuffling should now be on.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ToggleShuffleCommandTestSuite) TestExecuteWhenShuffleIsOn() {
|
||||||
|
viper.Set("queue.automatic_shuffle_on", true)
|
||||||
|
|
||||||
|
message, isPrivateMessage, err := suite.Command.Execute(nil)
|
||||||
|
|
||||||
|
suite.NotEqual("", message, "A message should be returned.")
|
||||||
|
suite.False(isPrivateMessage, "This should not be a private message.")
|
||||||
|
suite.Nil(err, "No error should be returned.")
|
||||||
|
suite.False(viper.GetBool("queue.automatic_shuffle_on"), "Automatic shuffling should now be off.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToggleShuffleCommandTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(ToggleShuffleCommandTestSuite))
|
||||||
|
}
|
47
commands/version.go
Normal file
47
commands/version.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/version.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VersionCommand is a command that outputs the local MumbleDJ version.
|
||||||
|
type VersionCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *VersionCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.version.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *VersionCommand) Description() string {
|
||||||
|
return viper.GetString("commands.version.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *VersionCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.version.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *VersionCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
return fmt.Sprintf("MumbleDJ version: <b>%s</b>", DJ.Version), true, nil
|
||||||
|
}
|
55
commands/version_test.go
Normal file
55
commands/version_test.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/version_test.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/matthieugrieger/mumbledj/bot"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VersionCommandTestSuite struct {
|
||||||
|
Command VersionCommand
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *VersionCommandTestSuite) SetupSuite() {
|
||||||
|
DJ = bot.NewMumbleDJ()
|
||||||
|
|
||||||
|
viper.Set("commands.version.aliases", []string{"version", "v"})
|
||||||
|
viper.Set("commands.version.description", "version")
|
||||||
|
viper.Set("commands.version.is_admin", false)
|
||||||
|
DJ.Version = "test"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *VersionCommandTestSuite) TestAliases() {
|
||||||
|
suite.Equal([]string{"version", "v"}, suite.Command.Aliases())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *VersionCommandTestSuite) TestDescription() {
|
||||||
|
suite.Equal("version", suite.Command.Description())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *VersionCommandTestSuite) TestIsAdminCommand() {
|
||||||
|
suite.False(suite.Command.IsAdminCommand())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *VersionCommandTestSuite) TestExecute() {
|
||||||
|
message, isPrivateMessage, err := suite.Command.Execute(nil)
|
||||||
|
|
||||||
|
suite.NotEqual("", message, "A message should be returned.")
|
||||||
|
suite.Contains(message, "MumbleDJ", "The message should contain a MumbleDJ version string.")
|
||||||
|
suite.True(isPrivateMessage, "This should be a private message.")
|
||||||
|
suite.Nil(err, "No error should be returned.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVersionCommandTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(VersionCommandTestSuite))
|
||||||
|
}
|
72
commands/volume.go
Normal file
72
commands/volume.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/volume.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VolumeCommand is a command that changes the volume of the audio output.
|
||||||
|
type VolumeCommand struct{}
|
||||||
|
|
||||||
|
// Aliases returns the current aliases for the command.
|
||||||
|
func (c *VolumeCommand) Aliases() []string {
|
||||||
|
return viper.GetStringSlice("commands.volume.aliases")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Description returns the description for the command.
|
||||||
|
func (c *VolumeCommand) Description() string {
|
||||||
|
return viper.GetString("commands.volume.description")
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAdminCommand returns true if the command is only for admin use, and
|
||||||
|
// returns false otherwise.
|
||||||
|
func (c *VolumeCommand) IsAdminCommand() bool {
|
||||||
|
return viper.GetBool("commands.volume.is_admin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the command with the given user and arguments.
|
||||||
|
// Return value descriptions:
|
||||||
|
// string: A message to be returned to the user upon successful execution.
|
||||||
|
// bool: Whether the message should be private or not. true = private,
|
||||||
|
// false = public (sent to whole channel).
|
||||||
|
// error: An error message to be returned upon unsuccessful execution.
|
||||||
|
// If no error has occurred, pass nil instead.
|
||||||
|
// Example return statement:
|
||||||
|
// return "This is a private message!", true, nil
|
||||||
|
func (c *VolumeCommand) Execute(user *gumble.User, args ...string) (string, bool, error) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
// Send the user the current volume level.
|
||||||
|
return fmt.Sprintf("The current volume is <b>%.2f</b>.", DJ.Volume), true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
newVolume, err := strconv.ParseFloat(args[0], 32)
|
||||||
|
if err != nil {
|
||||||
|
return "", true, errors.New("An error occurred while parsing the requested volume")
|
||||||
|
}
|
||||||
|
|
||||||
|
if newVolume < viper.GetFloat64("volume.lowest") || newVolume > viper.GetFloat64("volume.highest") {
|
||||||
|
return "", true, fmt.Errorf("Volumes must be between the values <b>%.2f</b> and <b>%.2f</b>",
|
||||||
|
viper.GetFloat64("volume.lowest"), viper.GetFloat64("volume.highest"))
|
||||||
|
}
|
||||||
|
|
||||||
|
newVolume32 := float32(newVolume)
|
||||||
|
|
||||||
|
if DJ.AudioStream != nil {
|
||||||
|
DJ.AudioStream.Volume = newVolume32
|
||||||
|
}
|
||||||
|
DJ.Volume = newVolume32
|
||||||
|
|
||||||
|
return fmt.Sprintf("<b>%s</b> has changed the volume to <b>%.2f</b>.",
|
||||||
|
user.Name, newVolume32), false, nil
|
||||||
|
}
|
91
commands/volume_test.go
Normal file
91
commands/volume_test.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* commands/volume_test.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/matthieugrieger/mumbledj/bot"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VolumeCommandTestSuite struct {
|
||||||
|
Command VolumeCommand
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *VolumeCommandTestSuite) SetupSuite() {
|
||||||
|
DJ = bot.NewMumbleDJ()
|
||||||
|
|
||||||
|
viper.Set("commands.volume.aliases", []string{"volume", "vol"})
|
||||||
|
viper.Set("commands.volume.description", "volume")
|
||||||
|
viper.Set("commands.volume.is_admin", false)
|
||||||
|
viper.Set("volume.lowest", 0.2)
|
||||||
|
viper.Set("volume.highest", 1)
|
||||||
|
viper.Set("volume.default", 0.4)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *VolumeCommandTestSuite) SetupTest() {
|
||||||
|
DJ.Volume = float32(viper.GetFloat64("volume.default"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *VolumeCommandTestSuite) TestAliases() {
|
||||||
|
suite.Equal([]string{"volume", "vol"}, suite.Command.Aliases())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *VolumeCommandTestSuite) TestDescription() {
|
||||||
|
suite.Equal("volume", suite.Command.Description())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *VolumeCommandTestSuite) TestIsAdminCommand() {
|
||||||
|
suite.False(suite.Command.IsAdminCommand())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *VolumeCommandTestSuite) TestExecuteWithNoArgs() {
|
||||||
|
message, isPrivateMessage, err := suite.Command.Execute(nil)
|
||||||
|
|
||||||
|
suite.NotEqual("", message, "A message should be returned.")
|
||||||
|
suite.True(isPrivateMessage, "This should be a private message.")
|
||||||
|
suite.Nil(err, "No error should be returned.")
|
||||||
|
suite.Contains(message, "0.4", "The returned string should contain the current volume.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *VolumeCommandTestSuite) TestExecuteWithValidArg() {
|
||||||
|
dummyUser := &gumble.User{
|
||||||
|
Name: "test",
|
||||||
|
}
|
||||||
|
message, isPrivateMessage, err := suite.Command.Execute(dummyUser, "0.6")
|
||||||
|
|
||||||
|
suite.NotEqual("", message, "A message should be returned.")
|
||||||
|
suite.False(isPrivateMessage, "This should not be a private message.")
|
||||||
|
suite.Nil(err, "No error should be returned.")
|
||||||
|
suite.Contains(message, "0.6", "The returned string should contain the new volume.")
|
||||||
|
suite.Contains(message, "test", "The returned string should contain the username of whomever changed the volume.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *VolumeCommandTestSuite) TestExecuteWithArgOutOfRange() {
|
||||||
|
message, isPrivateMessage, err := suite.Command.Execute(nil, "1.4")
|
||||||
|
|
||||||
|
suite.Equal("", message, "No message should be returned as an error occurred.")
|
||||||
|
suite.True(isPrivateMessage, "This should be a private message.")
|
||||||
|
suite.NotNil(err, "An error should be returned since the provided argument was outside of the valid range.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *VolumeCommandTestSuite) TestExecuteWithInvalidArg() {
|
||||||
|
message, isPrivateMessage, err := suite.Command.Execute(nil, "test")
|
||||||
|
|
||||||
|
suite.Equal("", message, "No message should be returned as an error occurred.")
|
||||||
|
suite.True(isPrivateMessage, "This should be a private message.")
|
||||||
|
suite.NotNil(err, "An error should be returned as a non-floating-point argument was provided.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVolumeCommandTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(VolumeCommandTestSuite))
|
||||||
|
}
|
274
config.gcfg
274
config.gcfg
|
@ -1,274 +0,0 @@
|
||||||
# MumbleDJ
|
|
||||||
# By Matthieu Grieger
|
|
||||||
# config.gcfg
|
|
||||||
# Copyright (c) 2014 Matthieu Grieger (MIT License)
|
|
||||||
|
|
||||||
[General]
|
|
||||||
|
|
||||||
# Command prefix
|
|
||||||
# DEFAULT VALUE: "!"
|
|
||||||
CommandPrefix = "!"
|
|
||||||
|
|
||||||
# Ratio that must be met or exceeded to trigger a song skip
|
|
||||||
# DEFAULT VALUE: 0.5
|
|
||||||
SkipRatio = 0.5
|
|
||||||
|
|
||||||
# Ratio that must be met or exceeded to trigger a playlist skip
|
|
||||||
# DEFAULT VALUE: 0.5
|
|
||||||
PlaylistSkipRatio = 0.5
|
|
||||||
|
|
||||||
# Default comment to be applied to bot.
|
|
||||||
# DEFAULT VALUE: "Hello! I am a bot. Type !help for a list of commands."
|
|
||||||
# NOTE: If you do not want a comment by default, set the variable equal to an empty string ("").
|
|
||||||
DefaultComment = "Hello! I am a bot. Type !help for a list of commands."
|
|
||||||
|
|
||||||
# Maximum song duration in seconds (0 = unrestricted)
|
|
||||||
# Default Value: 0
|
|
||||||
MaxSongDuration = 0
|
|
||||||
|
|
||||||
# Maximum songs per playlist (0 = unrestricted)
|
|
||||||
# Default Value: 50
|
|
||||||
MaxSongPerPlaylist = 50
|
|
||||||
|
|
||||||
# Is playlist shuffling enabled when the bot starts?
|
|
||||||
# Default Value: false
|
|
||||||
AutomaticShuffleOn = false
|
|
||||||
|
|
||||||
# Announce song information at start of track
|
|
||||||
# Default Value: true
|
|
||||||
AnnounceNewTrack = true
|
|
||||||
|
|
||||||
# Command to use to play audio files. The two supported choices are "ffmpeg" and "avconv".
|
|
||||||
# Default Value: "ffmpeg"
|
|
||||||
PlayerCommand = "ffmpeg"
|
|
||||||
|
|
||||||
[Cache]
|
|
||||||
|
|
||||||
# Cache songs as they are downloaded?
|
|
||||||
# DEFAULT VALUE: false
|
|
||||||
Enabled = false
|
|
||||||
|
|
||||||
# Maximum total file size of cache directory (in MB)
|
|
||||||
# DEFAULT VALUE: 512
|
|
||||||
MaximumSize = 512
|
|
||||||
|
|
||||||
# Period of time that should elapse before a song is cleared from the cache (in hours)
|
|
||||||
# DEFAULT VALUE: 24
|
|
||||||
ExpireTime = 24
|
|
||||||
|
|
||||||
|
|
||||||
[Volume]
|
|
||||||
|
|
||||||
# Default volume
|
|
||||||
# DEFAULT VALUE: 0.2
|
|
||||||
DefaultVolume = 0.2
|
|
||||||
|
|
||||||
# Lowest volume allowed
|
|
||||||
# DEFAULT VALUE: 0.01
|
|
||||||
LowestVolume = 0.01
|
|
||||||
|
|
||||||
# Highest volume allowed
|
|
||||||
# DEFAULT VALUE: 0.8
|
|
||||||
HighestVolume = 0.8
|
|
||||||
|
|
||||||
|
|
||||||
[Aliases]
|
|
||||||
|
|
||||||
# Alias used for add command
|
|
||||||
# DEFAULT VALUE: "add"
|
|
||||||
AddAlias = "add"
|
|
||||||
|
|
||||||
# Alias used for addnext command
|
|
||||||
# DEFAULT VALUE: "addnext"
|
|
||||||
AddNextAlias = "addnext"
|
|
||||||
|
|
||||||
# Alias used for skip command
|
|
||||||
# DEFAULT VALUE: "skip"
|
|
||||||
SkipAlias = "skip"
|
|
||||||
|
|
||||||
# Alias used for playlist skip command
|
|
||||||
# DEFAULT VALUE: "skipplaylist"
|
|
||||||
SkipPlaylistAlias = "skipplaylist"
|
|
||||||
|
|
||||||
# Alias used for admin skip command
|
|
||||||
# DEFAULT VALUE: "forceskip"
|
|
||||||
AdminSkipAlias = "forceskip"
|
|
||||||
|
|
||||||
# Alias used for admin playlist skip command
|
|
||||||
# DEFAULT VALUE: "forceskipplaylist"
|
|
||||||
AdminSkipPlaylistAlias = "forceskipplaylist"
|
|
||||||
|
|
||||||
# Alias used for help command
|
|
||||||
# DEFAULT VALUE: "help"
|
|
||||||
HelpAlias = "help"
|
|
||||||
|
|
||||||
# Alias used for volume command
|
|
||||||
# DEFAULT VALUE: "volume"
|
|
||||||
VolumeAlias = "volume"
|
|
||||||
|
|
||||||
# Alias used for joinme
|
|
||||||
# DEFAULT VALUE : "joinme"
|
|
||||||
JoinMeAlias = "joinme"
|
|
||||||
|
|
||||||
# Alias used for move command
|
|
||||||
# DEFAULT VALUE: "move"
|
|
||||||
MoveAlias = "move"
|
|
||||||
|
|
||||||
# Alias used for reload command
|
|
||||||
# DEFAULT VALUE: "reload"
|
|
||||||
ReloadAlias = "reload"
|
|
||||||
|
|
||||||
# Alias used for queue reset command
|
|
||||||
# DEFAULT VALUE: "reset"
|
|
||||||
ResetAlias = "reset"
|
|
||||||
|
|
||||||
# Alias used for numsongs command
|
|
||||||
# DEFAULT VALUE: "numsongs"
|
|
||||||
NumSongsAlias = "numsongs"
|
|
||||||
|
|
||||||
# Alias used for nextsong command
|
|
||||||
# DEFAULT VALUE: "nextsong"
|
|
||||||
NextSongAlias = "nextsong"
|
|
||||||
|
|
||||||
# Alias used for the currentsong command
|
|
||||||
# DEFAULT VALUE: "currentsong"
|
|
||||||
CurrentSongAlias = "currentsong"
|
|
||||||
|
|
||||||
# Alias used for the setcomment command
|
|
||||||
# DEFAULT VALUE: "setcomment"
|
|
||||||
SetCommentAlias = "setcomment"
|
|
||||||
|
|
||||||
# Alias used for numcached command
|
|
||||||
# DEFAULT VALUE: "numcached"
|
|
||||||
NumCachedAlias = "numcached"
|
|
||||||
|
|
||||||
# Alias used for cachesize command
|
|
||||||
# DEFAULT VALUE: "cachesize"
|
|
||||||
CacheSizeAlias = "cachesize"
|
|
||||||
|
|
||||||
# Alias used for kill command
|
|
||||||
# DEFAULT VALUE: "kill"
|
|
||||||
KillAlias = "kill"
|
|
||||||
|
|
||||||
# Alias used for shuffle command
|
|
||||||
# DEFAULT VALUE: "shuffle"
|
|
||||||
ShuffleAlias = "shuffle"
|
|
||||||
|
|
||||||
# Alias used for shuffleon command
|
|
||||||
# DEFAULT VALUE: "shuffleon"
|
|
||||||
ShuffleOnAlias = "shuffleon"
|
|
||||||
|
|
||||||
# Alias used for shuffleoff command
|
|
||||||
# DEFAULT VALUE: "shuffleoff"
|
|
||||||
ShuffleOffAlias = "shuffleoff"
|
|
||||||
|
|
||||||
# Alias used for listsongs command
|
|
||||||
# DEFAULT_VALUE: "listsongs"
|
|
||||||
ListSongsAlias = "listsongs"
|
|
||||||
|
|
||||||
# Alias used for version command
|
|
||||||
# DEFAULT_VALUE: "version"
|
|
||||||
VersionAlias = "version"
|
|
||||||
|
|
||||||
[Permissions]
|
|
||||||
|
|
||||||
# Enable admins
|
|
||||||
# DEFAULT VALUE: true
|
|
||||||
AdminsEnabled = true
|
|
||||||
|
|
||||||
# List of admins
|
|
||||||
# NOTE: I recommend only giving users admin privileges if they are registered
|
|
||||||
# on the server. Otherwise people can just take their username and issue admin
|
|
||||||
# commands.
|
|
||||||
# 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"
|
|
||||||
|
|
||||||
# Make add an admin command?
|
|
||||||
# DEFAULT VALUE: false
|
|
||||||
AdminAdd = false
|
|
||||||
|
|
||||||
# Make addnext an admin command?
|
|
||||||
# DEFAULT VALUE: true
|
|
||||||
AdminAddNext = true
|
|
||||||
|
|
||||||
# Make playlist adds an admin only action?
|
|
||||||
# DEFAULT VALUE: false
|
|
||||||
AdminAddPlaylists = false
|
|
||||||
|
|
||||||
# Make skip an admin command?
|
|
||||||
# DEFAULT VALUE: false
|
|
||||||
AdminSkip = false
|
|
||||||
|
|
||||||
# Make help an admin command?
|
|
||||||
# DEFAULT VALUE: false
|
|
||||||
AdminHelp = false
|
|
||||||
|
|
||||||
# Make volume an admin command?
|
|
||||||
# DEFAULT VALUE: false
|
|
||||||
AdminVolume = false
|
|
||||||
|
|
||||||
# Make move an admin command?
|
|
||||||
# DEFAULT VALUE: true
|
|
||||||
AdminMove = true
|
|
||||||
|
|
||||||
# Make joinme a admin command?
|
|
||||||
# DEFAULT VALUE: true
|
|
||||||
AdminJoinMe = true
|
|
||||||
|
|
||||||
# Make reload an admin command?
|
|
||||||
# DEFAULT VALUE: true
|
|
||||||
AdminReload = true
|
|
||||||
|
|
||||||
# Make reset an admin command?
|
|
||||||
# DEFAULT VALUE: true
|
|
||||||
AdminReset = true
|
|
||||||
|
|
||||||
# Make numsongs an admin command?
|
|
||||||
# DEFAULT VALUE: false
|
|
||||||
AdminNumSongs = false
|
|
||||||
|
|
||||||
# Make nextsong an admin command?
|
|
||||||
# DEFAULT VALUE: false
|
|
||||||
AdminNextSong = false
|
|
||||||
|
|
||||||
# Make currentsong an admin command?
|
|
||||||
# DEFAULT VALUE: false
|
|
||||||
AdminCurrentSong = false
|
|
||||||
|
|
||||||
# Make setcomment an admin command?
|
|
||||||
# DEFAULT VALUE: true
|
|
||||||
AdminSetComment = true
|
|
||||||
|
|
||||||
# Make numcached an admin command?
|
|
||||||
# DEFAULT VALUE: true
|
|
||||||
AdminNumCached = true
|
|
||||||
|
|
||||||
# Make cachesize an admin command?
|
|
||||||
# DEFAULT VALUE: true
|
|
||||||
AdminCacheSize = true
|
|
||||||
|
|
||||||
# Make kill an admin command?
|
|
||||||
# DEFAULT VALUE: true (I recommend never changing this to false)
|
|
||||||
AdminKill = true
|
|
||||||
|
|
||||||
# Make shuffle an admin command?
|
|
||||||
# DEFAULT VALUE: true
|
|
||||||
AdminShuffle = true
|
|
||||||
|
|
||||||
# Make listSongs an admin command?
|
|
||||||
# DEFAULT VALUE: false
|
|
||||||
AdminListSongs = false
|
|
||||||
|
|
||||||
# Make version an admin command?
|
|
||||||
# DEFAULT VALUE: false
|
|
||||||
AdminVersion = false
|
|
||||||
|
|
||||||
# Make shuffleon and shuffleoff admin commands?
|
|
||||||
# DEFAULT VALUE: true
|
|
||||||
AdminShuffleToggle = true
|
|
||||||
|
|
||||||
[ServiceKeys]
|
|
||||||
YouTube = ""
|
|
||||||
SoundCloud = ""
|
|
328
config.yaml
Normal file
328
config.yaml
Normal file
|
@ -0,0 +1,328 @@
|
||||||
|
# MumbleDJ
|
||||||
|
# By Matthieu Grieger
|
||||||
|
# Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
# config.yaml
|
||||||
|
|
||||||
|
api_keys:
|
||||||
|
|
||||||
|
# YouTube API key.
|
||||||
|
youtube: ""
|
||||||
|
|
||||||
|
# SoundCloud API key.
|
||||||
|
# NOTE: The API key is your client ID.
|
||||||
|
soundcloud: ""
|
||||||
|
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
|
||||||
|
# Default comment to be applied to bot.
|
||||||
|
# NOTE: If you do not want a comment by default, set to empty string ("").
|
||||||
|
comment: "Hello! I am a bot. Type !help for a list of commands."
|
||||||
|
|
||||||
|
# Default channel for the bot to enter upon connection.
|
||||||
|
# NOTE: If you wish for the bot to connect to the root channel, set to empty string ("").
|
||||||
|
channel: ""
|
||||||
|
|
||||||
|
# Command to use to play audio files. The two supported choices are "ffmpeg" and "avconv".
|
||||||
|
player_command: "ffmpeg"
|
||||||
|
|
||||||
|
|
||||||
|
queue:
|
||||||
|
|
||||||
|
# Ratio that must be met or exceeded to trigger a track skip.
|
||||||
|
track_skip_ratio: 0.5
|
||||||
|
|
||||||
|
# Ratio that must be met or exceeded to trigger a playlist skip.
|
||||||
|
playlist_skip_ratio: 0.5
|
||||||
|
|
||||||
|
# Maximum track duration in seconds. Set to 0 for unrestricted duration.
|
||||||
|
max_track_duration: 0
|
||||||
|
|
||||||
|
# Maximum tracks per playlist. Set to 0 for unrestricted playlists.
|
||||||
|
max_tracks_per_playlist: 50
|
||||||
|
|
||||||
|
# Is shuffling enabled when the bot starts?
|
||||||
|
automatic_shuffle_on: false
|
||||||
|
|
||||||
|
# Announce track information at the beginning of audio playback?
|
||||||
|
announce_new_tracks: true
|
||||||
|
|
||||||
|
|
||||||
|
connection:
|
||||||
|
|
||||||
|
# Address bot should attempt to connect to.
|
||||||
|
address: "127.0.0.1"
|
||||||
|
|
||||||
|
# Port bot should attempt to connect to.
|
||||||
|
port: "64738"
|
||||||
|
|
||||||
|
# Password for connecting to server.
|
||||||
|
# NOTE: If no password, set to empty string ("").
|
||||||
|
password: ""
|
||||||
|
|
||||||
|
# Username for MumbleDJ.
|
||||||
|
username: "MumbleDJ"
|
||||||
|
|
||||||
|
# Should the bot attempt an insecure connection?
|
||||||
|
# An insecure connection does not verify the certificate of the server for
|
||||||
|
# consistency. It is best to leave this on, but disable it if you are having
|
||||||
|
# issues connecting to a server or are running multiple instances of MumbleDJ.
|
||||||
|
insecure: false
|
||||||
|
|
||||||
|
# Filepath to certificate file.
|
||||||
|
# NOTE: If no certificate file is needed, set to empty string ("").
|
||||||
|
cert: ""
|
||||||
|
|
||||||
|
# Filepath to certificate key file.
|
||||||
|
# NOTE: If no key is needed, set to empty string ("").
|
||||||
|
key: ""
|
||||||
|
|
||||||
|
# Access tokens to initialize the bot with, separated by commas.
|
||||||
|
# NOTE: If no access tokens are needed, set to empty string ("").
|
||||||
|
access_tokens: ""
|
||||||
|
|
||||||
|
# Should the bot automatically attempt to retry connection to a server after disconnecting?
|
||||||
|
retry_enabled: true
|
||||||
|
|
||||||
|
# How many times should the bot attempt to reconnect to the server?
|
||||||
|
retry_attempts: 10
|
||||||
|
|
||||||
|
# How many seconds should the bot wait in-between connection retry attempts?
|
||||||
|
retry_interval: 5
|
||||||
|
|
||||||
|
|
||||||
|
cache:
|
||||||
|
|
||||||
|
# Cache songs as they are downloaded?
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
# Maximum total file size of cache directory in MiB.
|
||||||
|
maximum_size: 512
|
||||||
|
|
||||||
|
# Period of time that should elapse before a song is cleared from the cache, in hours.
|
||||||
|
expire_time: 24
|
||||||
|
|
||||||
|
# Period of time between each check of the cache for expired items, in minutes.
|
||||||
|
check_interval: 5
|
||||||
|
|
||||||
|
# Directory to store cached items. Environment variables are able to be used here.
|
||||||
|
directory: "$HOME/.cache/mumbledj"
|
||||||
|
|
||||||
|
|
||||||
|
volume:
|
||||||
|
|
||||||
|
# Default volume.
|
||||||
|
default: 0.2
|
||||||
|
|
||||||
|
# Lowest volume allowed.
|
||||||
|
lowest: 0.01
|
||||||
|
|
||||||
|
# Highest volume allowed.
|
||||||
|
highest: 0.8
|
||||||
|
|
||||||
|
|
||||||
|
admins:
|
||||||
|
|
||||||
|
# Enable admins?
|
||||||
|
# NOTE: If this is set to false, any command can be executed by any user.
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# List of admin names.
|
||||||
|
# NOTE: It is recommended that the names in this list are registered on the
|
||||||
|
# server so that imposters cannot execute admin commands.
|
||||||
|
names:
|
||||||
|
- "SuperUser"
|
||||||
|
|
||||||
|
|
||||||
|
commands:
|
||||||
|
|
||||||
|
# Character used to designate commands from normal text messages.
|
||||||
|
# NOTE: Only one character (the first) is used.
|
||||||
|
prefix: "!"
|
||||||
|
|
||||||
|
# Below is a list of the commands supported by MumbleDJ. Each command has
|
||||||
|
# three configurable options:
|
||||||
|
# aliases: A list of names that can be used to execute the command.
|
||||||
|
# is_admin: true = only admins can execute the command, false = anyone can execute the command.
|
||||||
|
# description: Description shown for the command when the help command is executed.
|
||||||
|
add:
|
||||||
|
aliases:
|
||||||
|
- "add"
|
||||||
|
- "a"
|
||||||
|
is_admin: false
|
||||||
|
description: "Adds a track or playlist from a media site to the queue."
|
||||||
|
|
||||||
|
addnext:
|
||||||
|
aliases:
|
||||||
|
- "addnext"
|
||||||
|
- "an"
|
||||||
|
is_admin: true
|
||||||
|
description: "Adds a track or playlist from a media site as the next item in the queue."
|
||||||
|
|
||||||
|
cachesize:
|
||||||
|
aliases:
|
||||||
|
- "cachesize"
|
||||||
|
- "cs"
|
||||||
|
is_admin: true
|
||||||
|
description: "Outputs the file size of the cache in MiB if caching is enabled."
|
||||||
|
|
||||||
|
currenttrack:
|
||||||
|
aliases:
|
||||||
|
- "currenttrack"
|
||||||
|
- "currentsong"
|
||||||
|
- "current"
|
||||||
|
is_admin: false
|
||||||
|
description: "Outputs information about the current track in the queue if one exists."
|
||||||
|
|
||||||
|
forceskip:
|
||||||
|
aliases:
|
||||||
|
- "forceskip"
|
||||||
|
- "fs"
|
||||||
|
is_admin: true
|
||||||
|
description: "Immediately skips the current track."
|
||||||
|
|
||||||
|
forceskipplaylist:
|
||||||
|
aliases:
|
||||||
|
- "forceskipplaylist"
|
||||||
|
- "fsp"
|
||||||
|
is_admin: true
|
||||||
|
description: "Immediately skips the current playlist."
|
||||||
|
|
||||||
|
help:
|
||||||
|
aliases:
|
||||||
|
- "help"
|
||||||
|
- "h"
|
||||||
|
is_admin: false
|
||||||
|
description: "Outputs this list of commands."
|
||||||
|
|
||||||
|
joinme:
|
||||||
|
aliases:
|
||||||
|
- "joinme"
|
||||||
|
- "join"
|
||||||
|
is_admin: true
|
||||||
|
description: "Moves MumbleDJ into your current channel if not playing audio to someone else."
|
||||||
|
|
||||||
|
kill:
|
||||||
|
aliases:
|
||||||
|
- "kill"
|
||||||
|
- "k"
|
||||||
|
is_admin: true
|
||||||
|
description: "Stops the bot and cleans its cache directory."
|
||||||
|
|
||||||
|
listtracks:
|
||||||
|
aliases:
|
||||||
|
- "listtracks"
|
||||||
|
- "listsongs"
|
||||||
|
- "list"
|
||||||
|
- "l"
|
||||||
|
is_admin: false
|
||||||
|
description: "Outputs a list of the tracks currently in the queue."
|
||||||
|
|
||||||
|
move:
|
||||||
|
aliases:
|
||||||
|
- "move"
|
||||||
|
- "m"
|
||||||
|
is_admin: true
|
||||||
|
description: "Moves the bot into the Mumble channel provided via argument."
|
||||||
|
|
||||||
|
nexttrack:
|
||||||
|
aliases:
|
||||||
|
- "nexttrack"
|
||||||
|
- "nextsong"
|
||||||
|
- "next"
|
||||||
|
is_admin: false
|
||||||
|
description: "Outputs information about the next track in the queue if one exists."
|
||||||
|
|
||||||
|
numcached:
|
||||||
|
aliases:
|
||||||
|
- "numcached"
|
||||||
|
- "nc"
|
||||||
|
is_admin: true
|
||||||
|
description: "Outputs the number of tracks cached on disk if caching is enabled."
|
||||||
|
|
||||||
|
numtracks:
|
||||||
|
aliases:
|
||||||
|
- "numtracks"
|
||||||
|
- "numsongs"
|
||||||
|
- "nt"
|
||||||
|
is_admin: false
|
||||||
|
description: "Outputs the number of tracks currently in the queue."
|
||||||
|
|
||||||
|
pause:
|
||||||
|
aliases:
|
||||||
|
- "pause"
|
||||||
|
is_admin: false
|
||||||
|
description: "Pauses audio playback."
|
||||||
|
|
||||||
|
reload:
|
||||||
|
aliases:
|
||||||
|
- "reload"
|
||||||
|
- "r"
|
||||||
|
is_admin: true
|
||||||
|
description: "Reloads the configuration file."
|
||||||
|
|
||||||
|
reset:
|
||||||
|
aliases:
|
||||||
|
- "reset"
|
||||||
|
- "re"
|
||||||
|
is_admin: true
|
||||||
|
description: "Resets the queue by removing all queue items."
|
||||||
|
|
||||||
|
resume:
|
||||||
|
aliases:
|
||||||
|
- "resume"
|
||||||
|
is_admin: false
|
||||||
|
description: "Resumes audio playback."
|
||||||
|
|
||||||
|
setcomment:
|
||||||
|
aliases:
|
||||||
|
- "setcomment"
|
||||||
|
- "comment"
|
||||||
|
- "sc"
|
||||||
|
is_admin: true
|
||||||
|
description: "Sets the comment displayed next to MumbleDJ's username in Mumble."
|
||||||
|
|
||||||
|
shuffle:
|
||||||
|
aliases:
|
||||||
|
- "shuffle"
|
||||||
|
- "shuf"
|
||||||
|
- "sh"
|
||||||
|
is_admin: true
|
||||||
|
description: "Randomizes the tracks currently in the queue."
|
||||||
|
|
||||||
|
skip:
|
||||||
|
aliases:
|
||||||
|
- "skip"
|
||||||
|
- "s"
|
||||||
|
is_admin: false
|
||||||
|
description: "Places a vote to skip the current track."
|
||||||
|
|
||||||
|
skipplaylist:
|
||||||
|
aliases:
|
||||||
|
- "skipplaylist"
|
||||||
|
- "sp"
|
||||||
|
is_admin: false
|
||||||
|
description: "Places a vote to skip the current playlist."
|
||||||
|
|
||||||
|
toggleshuffle:
|
||||||
|
aliases:
|
||||||
|
- "toggleshuffle"
|
||||||
|
- "toggleshuf"
|
||||||
|
- "togshuf"
|
||||||
|
- "tsh"
|
||||||
|
is_admin: true
|
||||||
|
description: "Toggles automatic track shuffling on/off."
|
||||||
|
|
||||||
|
version:
|
||||||
|
aliases:
|
||||||
|
- "version"
|
||||||
|
- "v"
|
||||||
|
is_admin: false
|
||||||
|
description: "Outputs the current version of MumbleDJ."
|
||||||
|
|
||||||
|
volume:
|
||||||
|
aliases:
|
||||||
|
- "volume"
|
||||||
|
- "vol"
|
||||||
|
is_admin: false
|
||||||
|
description: "Changes the volume if an argument is provided, outputs the current volume otherwise."
|
62
glide.lock
generated
Normal file
62
glide.lock
generated
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
hash: 88cf7cf13ff239e97d3193606989de2e2972438e4ea17ad31755531f13acef50
|
||||||
|
updated: 2016-06-18T00:18:25.756228464-07:00
|
||||||
|
imports:
|
||||||
|
- name: github.com/antonholmquist/jason
|
||||||
|
version: 423803175a265e07c3f35cd8faf009eac345efb8
|
||||||
|
- name: github.com/BurntSushi/toml
|
||||||
|
version: f0aeabca5a127c4078abb8c8d64298b147264b55
|
||||||
|
- name: github.com/ChannelMeter/iso8601duration
|
||||||
|
version: 8da3af7a2a61a4eb5ae9bddec06bf637fa9593da
|
||||||
|
- name: github.com/fsnotify/fsnotify
|
||||||
|
version: 30411dbcefb7a1da7e84f75530ad3abe4011b4f8
|
||||||
|
- name: github.com/golang/protobuf
|
||||||
|
version: 0c1f6d65b5a189c2250d10e71a5506f06f9fa0a0
|
||||||
|
subpackages:
|
||||||
|
- proto
|
||||||
|
- name: github.com/hashicorp/hcl
|
||||||
|
version: aa7699b7b62c5f410f4cf7b58f3f9b17a71fb4ad
|
||||||
|
subpackages:
|
||||||
|
- hcl/ast
|
||||||
|
- hcl/parser
|
||||||
|
- hcl/token
|
||||||
|
- json/parser
|
||||||
|
- hcl/scanner
|
||||||
|
- hcl/strconv
|
||||||
|
- json/scanner
|
||||||
|
- json/token
|
||||||
|
- name: github.com/layeh/gopus
|
||||||
|
version: 867541549ca5f8b4db2b92fd1dded8711256a27d
|
||||||
|
- name: github.com/layeh/gumble
|
||||||
|
version: b26208eca4b75d9efdacb068241c68912437bd69
|
||||||
|
subpackages:
|
||||||
|
- gumble
|
||||||
|
- gumbleffmpeg
|
||||||
|
- gumbleutil
|
||||||
|
- opus
|
||||||
|
- gumble/MumbleProto
|
||||||
|
- gumble/varint
|
||||||
|
- name: github.com/magiconair/properties
|
||||||
|
version: c265cfa48dda6474e208715ca93e987829f572f8
|
||||||
|
- name: github.com/mitchellh/mapstructure
|
||||||
|
version: d2dd0262208475919e1a362f675cfc0e7c10e905
|
||||||
|
- name: github.com/Sirupsen/logrus
|
||||||
|
version: f3cfb454f4c209e6668c95216c4744b8fddb2356
|
||||||
|
- name: github.com/spf13/cast
|
||||||
|
version: 27b586b42e29bec072fe7379259cc719e1289da6
|
||||||
|
- name: github.com/spf13/jwalterweatherman
|
||||||
|
version: 33c24e77fb80341fe7130ee7c594256ff08ccc46
|
||||||
|
- name: github.com/spf13/pflag
|
||||||
|
version: 367864438f1b1a3c7db4da06a2f55b144e6784e0
|
||||||
|
- name: github.com/spf13/viper
|
||||||
|
version: c1ccc378a054ea8d4e38d8c67f6938d4760b53dd
|
||||||
|
- name: github.com/stretchr/testify
|
||||||
|
version: d77da356e56a7428ad25149ca77381849a6a5232
|
||||||
|
- name: github.com/urfave/cli
|
||||||
|
version: b438abf775124d32be72016935a9553dbffe8473
|
||||||
|
- name: golang.org/x/sys
|
||||||
|
version: 62bee037599929a6e9146f29d10dd5208c43507d
|
||||||
|
subpackages:
|
||||||
|
- unix
|
||||||
|
- name: gopkg.in/yaml.v2
|
||||||
|
version: a83829b6f1293c91addabc89d0571c246397bbf4
|
||||||
|
devImports: []
|
20
glide.yaml
Normal file
20
glide.yaml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package: github.com/matthieugrieger/mumbledj
|
||||||
|
homepage: https://github.com/matthieugrieger/mumbledj
|
||||||
|
license: MIT
|
||||||
|
owners:
|
||||||
|
- name: Matthieu Grieger
|
||||||
|
email: me@matthieugrieger.com
|
||||||
|
homepage: https://matthieugrieger.com
|
||||||
|
import:
|
||||||
|
- package: github.com/ChannelMeter/iso8601duration
|
||||||
|
- package: github.com/Sirupsen/logrus
|
||||||
|
- package: github.com/antonholmquist/jason
|
||||||
|
- package: github.com/layeh/gumble
|
||||||
|
subpackages:
|
||||||
|
- gumble
|
||||||
|
- gumbleffmpeg
|
||||||
|
- gumbleutil
|
||||||
|
- opus
|
||||||
|
- package: github.com/spf13/viper
|
||||||
|
- package: github.com/urfave/cli
|
||||||
|
- package: github.com/stretchr/testify
|
18
interfaces/command.go
Normal file
18
interfaces/command.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* interfaces/command.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package interfaces
|
||||||
|
|
||||||
|
import "github.com/layeh/gumble/gumble"
|
||||||
|
|
||||||
|
// Command is an interface that all commands must implement.
|
||||||
|
type Command interface {
|
||||||
|
Aliases() []string
|
||||||
|
Description() string
|
||||||
|
IsAdminCommand() bool
|
||||||
|
Execute(user *gumble.User, args ...string) (string, bool, error)
|
||||||
|
}
|
16
interfaces/playlist.go
Normal file
16
interfaces/playlist.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* interfaces/playlist.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package interfaces
|
||||||
|
|
||||||
|
// Playlist is an interface of methods that must be implemented by playlists.
|
||||||
|
type Playlist interface {
|
||||||
|
GetID() string
|
||||||
|
GetTitle() string
|
||||||
|
GetSubmitter() string
|
||||||
|
GetService() string
|
||||||
|
}
|
29
interfaces/queue.go
Normal file
29
interfaces/queue.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* interfaces/queue.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package interfaces
|
||||||
|
|
||||||
|
// Queue is the interface which should be interacted with for queue operations.
|
||||||
|
// Using the Queue interface ensures thread safety.
|
||||||
|
type Queue interface {
|
||||||
|
Length() int
|
||||||
|
Reset()
|
||||||
|
AppendTrack(Track) error
|
||||||
|
InsertTrack(int, Track) error
|
||||||
|
CurrentTrack() (Track, error)
|
||||||
|
GetTrack(int) Track
|
||||||
|
PeekNextTrack() (Track, error)
|
||||||
|
Traverse(func(int, Track))
|
||||||
|
ShuffleTracks()
|
||||||
|
RandomNextTrack(bool)
|
||||||
|
Skip()
|
||||||
|
SkipPlaylist()
|
||||||
|
PlayCurrent() error
|
||||||
|
PauseCurrent() error
|
||||||
|
ResumeCurrent() error
|
||||||
|
StopCurrent() error
|
||||||
|
}
|
20
interfaces/service.go
Normal file
20
interfaces/service.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* interfaces/service.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package interfaces
|
||||||
|
|
||||||
|
import "github.com/layeh/gumble/gumble"
|
||||||
|
|
||||||
|
// Service is an interface of methods to be implemented
|
||||||
|
// by various service types, such as YouTube or SoundCloud.
|
||||||
|
type Service interface {
|
||||||
|
GetReadableName() string
|
||||||
|
GetFormat() string
|
||||||
|
CheckAPIKey() error
|
||||||
|
CheckURL(string) bool
|
||||||
|
GetTracks(string, *gumble.User) ([]Track, error)
|
||||||
|
}
|
23
interfaces/skiptracker.go
Normal file
23
interfaces/skiptracker.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* interfaces/skiptracker.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package interfaces
|
||||||
|
|
||||||
|
import "github.com/layeh/gumble/gumble"
|
||||||
|
|
||||||
|
// SkipTracker is the interface which should be interacted with for skip operations.
|
||||||
|
// Using the SkipTracker interface ensures thread safety.
|
||||||
|
type SkipTracker interface {
|
||||||
|
AddTrackSkip(*gumble.User) error
|
||||||
|
AddPlaylistSkip(*gumble.User) error
|
||||||
|
RemoveTrackSkip(*gumble.User) error
|
||||||
|
RemovePlaylistSkip(*gumble.User) error
|
||||||
|
NumTrackSkips() int
|
||||||
|
NumPlaylistSkips() int
|
||||||
|
ResetTrackSkips()
|
||||||
|
ResetPlaylistSkips()
|
||||||
|
}
|
26
interfaces/track.go
Normal file
26
interfaces/track.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* interfaces/track.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package interfaces
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Track is an interface of methods that must be implemented by tracks.
|
||||||
|
type Track interface {
|
||||||
|
GetID() string
|
||||||
|
GetURL() string
|
||||||
|
GetTitle() string
|
||||||
|
GetAuthor() string
|
||||||
|
GetAuthorURL() string
|
||||||
|
GetSubmitter() string
|
||||||
|
GetService() string
|
||||||
|
GetFilename() string
|
||||||
|
GetThumbnailURL() string
|
||||||
|
GetDuration() time.Duration
|
||||||
|
GetPlaybackOffset() time.Duration
|
||||||
|
GetPlaylist() Playlist
|
||||||
|
}
|
443
main.go
443
main.go
|
@ -2,262 +2,225 @@
|
||||||
* MumbleDJ
|
* MumbleDJ
|
||||||
* By Matthieu Grieger
|
* By Matthieu Grieger
|
||||||
* main.go
|
* main.go
|
||||||
* Copyright (c) 2014, 2015 Matthieu Grieger (MIT License)
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"io/ioutil"
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/layeh/gopus"
|
"github.com/Sirupsen/logrus"
|
||||||
"github.com/layeh/gumble/gumble"
|
"github.com/matthieugrieger/mumbledj/bot"
|
||||||
"github.com/layeh/gumble/gumble_ffmpeg"
|
"github.com/matthieugrieger/mumbledj/commands"
|
||||||
"github.com/layeh/gumble/gumbleutil"
|
"github.com/matthieugrieger/mumbledj/services"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mumbledj is a struct that keeps track of all aspects of the bot's current
|
// DJ is a global variable that holds various details about the bot's state.
|
||||||
// state.
|
var DJ = bot.NewMumbleDJ()
|
||||||
type mumbledj struct {
|
|
||||||
config gumble.Config
|
func init() {
|
||||||
client *gumble.Client
|
DJ.Commands = commands.Commands
|
||||||
keepAlive chan bool
|
DJ.AvailableServices = services.Services
|
||||||
defaultChannel []string
|
|
||||||
conf DjConfig
|
// Injection into sub-packages.
|
||||||
queue *SongQueue
|
commands.DJ = DJ
|
||||||
audioStream *gumble_ffmpeg.Stream
|
services.DJ = DJ
|
||||||
homeDir string
|
bot.DJ = DJ
|
||||||
playlistSkips map[string][]string
|
|
||||||
cache *SongCache
|
DJ.Version = "3.0.0"
|
||||||
|
|
||||||
|
logrus.SetLevel(logrus.WarnLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnConnect event. First moves MumbleDJ into the default channel specified
|
|
||||||
// via commandline args, and moves to root channel if the channel does not exist. The current
|
|
||||||
// user's homedir path is stored, configuration is loaded, and the audio stream is set up.
|
|
||||||
func (dj *mumbledj) OnConnect(e *gumble.ConnectEvent) {
|
|
||||||
if dj.client.Channels.Find(dj.defaultChannel...) != nil {
|
|
||||||
dj.client.Self.Move(dj.client.Channels.Find(dj.defaultChannel...))
|
|
||||||
} else {
|
|
||||||
fmt.Println("Channel doesn't exist or one was not provided, staying in root channel...")
|
|
||||||
}
|
|
||||||
|
|
||||||
dj.audioStream = gumble_ffmpeg.New(dj.client)
|
|
||||||
dj.audioStream.Volume = dj.conf.Volume.DefaultVolume
|
|
||||||
|
|
||||||
if dj.conf.General.PlayerCommand == "ffmpeg" || dj.conf.General.PlayerCommand == "avconv" {
|
|
||||||
dj.audioStream.Command = dj.conf.General.PlayerCommand
|
|
||||||
} else {
|
|
||||||
fmt.Println("Invalid PlayerCommand configuration value. Only \"ffmpeg\" and \"avconv\" are supported. Defaulting to ffmpeg...")
|
|
||||||
}
|
|
||||||
|
|
||||||
dj.client.AudioEncoder.SetApplication(gopus.Audio)
|
|
||||||
|
|
||||||
dj.client.Self.SetComment(dj.conf.General.DefaultComment)
|
|
||||||
|
|
||||||
if dj.conf.Cache.Enabled {
|
|
||||||
dj.cache.Update()
|
|
||||||
go dj.cache.ClearExpired()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnDisconnect event. Terminates MumbleDJ thread.
|
|
||||||
func (dj *mumbledj) OnDisconnect(e *gumble.DisconnectEvent) {
|
|
||||||
if e.Type == gumble.DisconnectError || e.Type == gumble.DisconnectKicked {
|
|
||||||
fmt.Println("Disconnected from server... Will retry connection in 30 second intervals for 15 minutes.")
|
|
||||||
reconnectSuccess := false
|
|
||||||
for retries := 0; retries <= 30; retries++ {
|
|
||||||
fmt.Println("Retrying connection...")
|
|
||||||
if err := dj.client.Connect(); err == nil {
|
|
||||||
fmt.Println("Successfully reconnected to the server!")
|
|
||||||
reconnectSuccess = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(30 * time.Second)
|
|
||||||
}
|
|
||||||
if !reconnectSuccess {
|
|
||||||
fmt.Println("Could not reconnect to server. Exiting...")
|
|
||||||
dj.keepAlive <- true
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dj.keepAlive <- true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnTextMessage event. Checks for command prefix, and calls parseCommand if it exists. Ignores
|
|
||||||
// the incoming message otherwise.
|
|
||||||
func (dj *mumbledj) OnTextMessage(e *gumble.TextMessageEvent) {
|
|
||||||
plainMessage := gumbleutil.PlainText(&e.TextMessage)
|
|
||||||
if len(plainMessage) != 0 {
|
|
||||||
if plainMessage[0] == dj.conf.General.CommandPrefix[0] && plainMessage != dj.conf.General.CommandPrefix {
|
|
||||||
parseCommand(e.Sender, e.Sender.Name, plainMessage[1:])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnUserChange event. Checks UserChange type, and adjusts items such as skiplists to reflect
|
|
||||||
// the current status of the users on the server.
|
|
||||||
func (dj *mumbledj) OnUserChange(e *gumble.UserChangeEvent) {
|
|
||||||
if e.Type.Has(gumble.UserChangeDisconnected) {
|
|
||||||
if dj.audioStream.IsPlaying() {
|
|
||||||
if !isNil(dj.queue.CurrentSong().Playlist()) {
|
|
||||||
dj.queue.CurrentSong().Playlist().RemoveSkip(e.User.Name)
|
|
||||||
}
|
|
||||||
dj.queue.CurrentSong().RemoveSkip(e.User.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasPermission checks if username has the permissions to execute a command. Permissions are specified in
|
|
||||||
// mumbledj.gcfg.
|
|
||||||
func (dj *mumbledj) HasPermission(username string, command bool) bool {
|
|
||||||
if dj.conf.Permissions.AdminsEnabled && command {
|
|
||||||
for _, adminName := range dj.conf.Permissions.Admins {
|
|
||||||
if username == adminName {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendPrivateMessage sends a private message to a user. Essentially just checks if a user is still in the server
|
|
||||||
// before sending them the message.
|
|
||||||
func (dj *mumbledj) SendPrivateMessage(user *gumble.User, message string) {
|
|
||||||
if targetUser := dj.client.Self.Channel.Users.Find(user.Name); targetUser != nil {
|
|
||||||
targetUser.Send(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckAPIKeys enables the services with API keys in the environment varaibles
|
|
||||||
func CheckAPIKeys() {
|
|
||||||
anyDisabled := false
|
|
||||||
|
|
||||||
// Checks YouTube API key
|
|
||||||
if dj.conf.ServiceKeys.Youtube == "" {
|
|
||||||
anyDisabled = true
|
|
||||||
fmt.Printf("The youtube service has been disabled as you do not have a YouTube API key defined in your config file!\n")
|
|
||||||
} else {
|
|
||||||
services = append(services, YouTube{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks Soundcloud API key
|
|
||||||
if dj.conf.ServiceKeys.SoundCloud == "" {
|
|
||||||
anyDisabled = true
|
|
||||||
fmt.Printf("The soundcloud service has been disabled as you do not have a Soundcloud API key defined in your config file!\n")
|
|
||||||
} else {
|
|
||||||
services = append(services, SoundCloud{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks to see if any service was disabled
|
|
||||||
if anyDisabled {
|
|
||||||
fmt.Printf("Please see the following link for info on how to enable missing services: https://github.com/matthieugrieger/mumbledj\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exits application if no services are enabled
|
|
||||||
if services == nil {
|
|
||||||
fmt.Printf("No services are enabled, and thus closing\n")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// isNil checks to see if an object is nil
|
|
||||||
func isNil(a interface{}) bool {
|
|
||||||
defer func() { recover() }()
|
|
||||||
return a == nil || reflect.ValueOf(a).IsNil()
|
|
||||||
}
|
|
||||||
|
|
||||||
// dj variable declaration. This is done outside of main() to allow global use.
|
|
||||||
var dj = mumbledj{
|
|
||||||
keepAlive: make(chan bool),
|
|
||||||
queue: NewSongQueue(),
|
|
||||||
playlistSkips: make(map[string][]string),
|
|
||||||
cache: NewSongCache(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// main primarily performs startup tasks. Grabs and parses commandline
|
|
||||||
// args, sets up the gumble client and its listeners, and then connects to the server.
|
|
||||||
func main() {
|
func main() {
|
||||||
|
app := cli.NewApp()
|
||||||
if currentUser, err := user.Current(); err == nil {
|
app.Name = "MumbleDJ"
|
||||||
dj.homeDir = currentUser.HomeDir
|
app.Usage = "A Mumble bot that plays audio from various media sites."
|
||||||
|
app.Version = DJ.Version
|
||||||
|
app.Flags = []cli.Flag{
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "config, c",
|
||||||
|
Value: os.ExpandEnv("$HOME/.config/mumbledj/config.yaml"),
|
||||||
|
Usage: "location of MumbleDJ configuration file",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "server, s",
|
||||||
|
Value: "127.0.0.1",
|
||||||
|
Usage: "address of Mumble server to connect to",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "port, o",
|
||||||
|
Value: "64738",
|
||||||
|
Usage: "port of Mumble server to connect to",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "username, u",
|
||||||
|
Value: "MumbleDJ",
|
||||||
|
Usage: "username for the bot",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "password, p",
|
||||||
|
Value: "",
|
||||||
|
Usage: "password for the Mumble server",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "channel, n",
|
||||||
|
Value: "",
|
||||||
|
Usage: "channel the bot enters after connecting to the Mumble server",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "cert, e",
|
||||||
|
Value: "",
|
||||||
|
Usage: "path to PEM certificate",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "key, k",
|
||||||
|
Value: "",
|
||||||
|
Usage: "path to PEM key",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "accesstokens, a",
|
||||||
|
Value: "",
|
||||||
|
Usage: "list of access tokens separated by spaces",
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "insecure, i",
|
||||||
|
Usage: "if present, the bot will not check Mumble certs for consistency",
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "debug, d",
|
||||||
|
Usage: "if present, all debug messages will be shown",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
app.Action = func(c *cli.Context) error {
|
||||||
if err := loadConfiguration(); err == nil {
|
if c.Bool("debug") {
|
||||||
fmt.Println("Configuration successfully loaded!")
|
logrus.SetLevel(logrus.InfoLevel)
|
||||||
} else {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var address, port, username, password, channel, pemCert, pemKey, accesstokens string
|
|
||||||
var insecure bool
|
|
||||||
var version bool
|
|
||||||
|
|
||||||
flag.StringVar(&address, "server", "localhost", "address for Mumble server")
|
|
||||||
flag.StringVar(&port, "port", "64738", "port for Mumble server")
|
|
||||||
flag.StringVar(&username, "username", "MumbleDJ", "username of MumbleDJ on server")
|
|
||||||
flag.StringVar(&password, "password", "", "password for Mumble server (if needed)")
|
|
||||||
flag.StringVar(&channel, "channel", "root", "default channel for MumbleDJ")
|
|
||||||
flag.StringVar(&pemCert, "cert", "", "path to user PEM certificate 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.BoolVar(&insecure, "insecure", false, "skip certificate checking")
|
|
||||||
flag.BoolVar(&version, "version", false, "show version")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if version {
|
|
||||||
fmt.Printf("MumbleDJ %s\n", VERSION)
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
dj.config = gumble.Config{
|
|
||||||
Username: username,
|
|
||||||
Password: password,
|
|
||||||
Address: address + ":" + port,
|
|
||||||
Tokens: strings.Split(accesstokens, " "),
|
|
||||||
}
|
|
||||||
dj.client = gumble.NewClient(&dj.config)
|
|
||||||
|
|
||||||
dj.config.TLSConfig.InsecureSkipVerify = true
|
|
||||||
if !insecure {
|
|
||||||
gumbleutil.CertificateLockFile(dj.client, fmt.Sprintf("%s/.mumbledj/cert.lock", dj.homeDir))
|
|
||||||
}
|
|
||||||
if pemCert != "" {
|
|
||||||
if pemKey == "" {
|
|
||||||
pemKey = pemCert
|
|
||||||
}
|
}
|
||||||
if certificate, err := tls.LoadX509KeyPair(pemCert, pemKey); err != nil {
|
|
||||||
panic(err)
|
viper.SetConfigFile(c.String("config"))
|
||||||
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"file": c.String("config"),
|
||||||
|
"error": err.Error(),
|
||||||
|
}).Warnln("An error occurred while reading the configuration file. Using default configuration...")
|
||||||
|
if _, err := os.Stat(c.String("config")); os.IsNotExist(err) {
|
||||||
|
createConfigWhenNotExists()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
dj.config.TLSConfig.Certificates = append(dj.config.TLSConfig.Certificates, certificate)
|
if duplicateErr := bot.CheckForDuplicateAliases(); duplicateErr != nil {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"issue": duplicateErr.Error(),
|
||||||
|
}).Fatalln("An issue was discoverd in your configuration.")
|
||||||
|
}
|
||||||
|
createNewConfigIfNeeded()
|
||||||
|
viper.WatchConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.GlobalIsSet("server") {
|
||||||
|
viper.Set("connection.address", c.String("server"))
|
||||||
|
}
|
||||||
|
if c.GlobalIsSet("port") {
|
||||||
|
viper.Set("connection.port", c.String("port"))
|
||||||
|
}
|
||||||
|
if c.GlobalIsSet("username") {
|
||||||
|
viper.Set("connection.username", c.String("username"))
|
||||||
|
}
|
||||||
|
if c.GlobalIsSet("password") {
|
||||||
|
viper.Set("connection.password", c.String("password"))
|
||||||
|
}
|
||||||
|
if c.GlobalIsSet("channel") {
|
||||||
|
viper.Set("defaults.channel", c.String("channel"))
|
||||||
|
}
|
||||||
|
if c.GlobalIsSet("cert") {
|
||||||
|
viper.Set("connection.cert", c.String("cert"))
|
||||||
|
}
|
||||||
|
if c.GlobalIsSet("key") {
|
||||||
|
viper.Set("connection.key", c.String("key"))
|
||||||
|
}
|
||||||
|
if c.GlobalIsSet("accesstokens") {
|
||||||
|
viper.Set("connection.access_tokens", c.String("accesstokens"))
|
||||||
|
}
|
||||||
|
if c.GlobalIsSet("insecure") {
|
||||||
|
viper.Set("connection.insecure", c.Bool("insecure"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := DJ.Connect(); err != nil {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"error": err.Error(),
|
||||||
|
}).Fatalln("An error occurred while connecting to the server.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if viper.GetString("defaults.channel") != "" {
|
||||||
|
defaultChannel := strings.Split(viper.GetString("defaults.channel"), "/")
|
||||||
|
DJ.Client.Do(func() {
|
||||||
|
DJ.Client.Self.Move(DJ.Client.Channels.Find(defaultChannel...))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
DJ.Client.Do(func() {
|
||||||
|
DJ.Client.Self.SetComment(viper.GetString("defaults.comment"))
|
||||||
|
})
|
||||||
|
<-DJ.KeepAlive
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Run(os.Args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createConfigWhenNotExists() {
|
||||||
|
configFile, err := Asset("config.yaml")
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warnln("An error occurred while accessing config binary data. A new config file will not be written.")
|
||||||
|
} else {
|
||||||
|
filePath := os.ExpandEnv("$HOME/.config/mumbledj/config.yaml")
|
||||||
|
os.Mkdir(os.ExpandEnv("$HOME/.config/mumbledj"), 0777)
|
||||||
|
writeErr := ioutil.WriteFile(filePath, configFile, 0644)
|
||||||
|
if writeErr == nil {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"file_path": filePath,
|
||||||
|
}).Infoln("A default configuration file has been written.")
|
||||||
|
} else {
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"error": writeErr.Error(),
|
||||||
|
}).Warnln("An error occurred while writing a new config file.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNewConfigIfNeeded() {
|
||||||
|
newConfigPath := os.ExpandEnv("$HOME/.config/mumbledj/config.yaml.new")
|
||||||
|
|
||||||
|
// Check if we should write an updated config file to config.yaml.new.
|
||||||
|
if assetInfo, err := AssetInfo("config.yaml"); err == nil {
|
||||||
|
asset, _ := Asset("config.yaml")
|
||||||
|
if configFile, err := os.Open(os.ExpandEnv("$HOME/.config/mumbledj/config.yaml")); err == nil {
|
||||||
|
configInfo, _ := configFile.Stat()
|
||||||
|
defer configFile.Close()
|
||||||
|
if configNewFile, err := os.Open(newConfigPath); err == nil {
|
||||||
|
defer configNewFile.Close()
|
||||||
|
configNewInfo, _ := configNewFile.Stat()
|
||||||
|
if assetInfo.ModTime().Unix() > configNewInfo.ModTime().Unix() {
|
||||||
|
// The config asset is newer than the config.yaml.new file.
|
||||||
|
// Write a new config.yaml.new file.
|
||||||
|
ioutil.WriteFile(os.ExpandEnv(newConfigPath), asset, 0644)
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"file_path": newConfigPath,
|
||||||
|
}).Infoln("An updated default configuration file has been written.")
|
||||||
|
}
|
||||||
|
} else if assetInfo.ModTime().Unix() > configInfo.ModTime().Unix() {
|
||||||
|
// The config asset is newer than the existing config file.
|
||||||
|
// Write a config.yaml.new file.
|
||||||
|
ioutil.WriteFile(os.ExpandEnv(newConfigPath), asset, 0644)
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"file_path": newConfigPath,
|
||||||
|
}).Infoln("An updated default configuration file has been written.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dj.defaultChannel = strings.Split(channel, "/")
|
|
||||||
|
|
||||||
services = append(services, Mixcloud{})
|
|
||||||
CheckAPIKeys()
|
|
||||||
|
|
||||||
dj.client.Attach(gumbleutil.Listener{
|
|
||||||
Connect: dj.OnConnect,
|
|
||||||
Disconnect: dj.OnDisconnect,
|
|
||||||
TextMessage: dj.OnTextMessage,
|
|
||||||
UserChange: dj.OnUserChange,
|
|
||||||
})
|
|
||||||
dj.client.Attach(gumbleutil.AutoBitrate)
|
|
||||||
|
|
||||||
if err := dj.client.Connect(); err != nil {
|
|
||||||
fmt.Printf("Could not connect to Mumble server at %s:%s.\n", address, port)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
<-dj.keepAlive
|
|
||||||
}
|
}
|
||||||
|
|
104
parseconfig.go
104
parseconfig.go
|
@ -1,104 +0,0 @@
|
||||||
/*
|
|
||||||
* MumbleDJ
|
|
||||||
* By Matthieu Grieger
|
|
||||||
* parseconfig.go
|
|
||||||
* Copyright (c) 2014, 2015 Matthieu Grieger (MIT License)
|
|
||||||
*/
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/scalingdata/gcfg"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DjConfig is a Golang struct representation of mumbledj.gcfg file structure for parsing.
|
|
||||||
type DjConfig struct {
|
|
||||||
General struct {
|
|
||||||
CommandPrefix string
|
|
||||||
SkipRatio float32
|
|
||||||
PlaylistSkipRatio float32
|
|
||||||
DefaultComment string
|
|
||||||
MaxSongDuration int
|
|
||||||
MaxSongPerPlaylist int
|
|
||||||
AutomaticShuffleOn bool
|
|
||||||
AnnounceNewTrack bool
|
|
||||||
PlayerCommand string
|
|
||||||
}
|
|
||||||
Cache struct {
|
|
||||||
Enabled bool
|
|
||||||
MaximumSize int64
|
|
||||||
ExpireTime float64
|
|
||||||
}
|
|
||||||
Volume struct {
|
|
||||||
DefaultVolume float32
|
|
||||||
LowestVolume float32
|
|
||||||
HighestVolume float32
|
|
||||||
}
|
|
||||||
Aliases struct {
|
|
||||||
AddAlias string
|
|
||||||
AddNextAlias string
|
|
||||||
SkipAlias string
|
|
||||||
SkipPlaylistAlias string
|
|
||||||
AdminSkipAlias string
|
|
||||||
AdminSkipPlaylistAlias string
|
|
||||||
HelpAlias string
|
|
||||||
VolumeAlias string
|
|
||||||
MoveAlias string
|
|
||||||
JoinMeAlias string
|
|
||||||
ReloadAlias string
|
|
||||||
ResetAlias string
|
|
||||||
NumSongsAlias string
|
|
||||||
NextSongAlias string
|
|
||||||
CurrentSongAlias string
|
|
||||||
SetCommentAlias string
|
|
||||||
NumCachedAlias string
|
|
||||||
CacheSizeAlias string
|
|
||||||
KillAlias string
|
|
||||||
ShuffleAlias string
|
|
||||||
ShuffleOnAlias string
|
|
||||||
ShuffleOffAlias string
|
|
||||||
ListSongsAlias string
|
|
||||||
VersionAlias string
|
|
||||||
}
|
|
||||||
Permissions struct {
|
|
||||||
AdminsEnabled bool
|
|
||||||
Admins []string
|
|
||||||
AdminAdd bool
|
|
||||||
AdminAddNext bool
|
|
||||||
AdminAddPlaylists bool
|
|
||||||
AdminSkip bool
|
|
||||||
AdminHelp bool
|
|
||||||
AdminVolume bool
|
|
||||||
AdminMove bool
|
|
||||||
AdminJoinMe bool
|
|
||||||
AdminReload bool
|
|
||||||
AdminReset bool
|
|
||||||
AdminNumSongs bool
|
|
||||||
AdminNextSong bool
|
|
||||||
AdminCurrentSong bool
|
|
||||||
AdminSetComment bool
|
|
||||||
AdminNumCached bool
|
|
||||||
AdminCacheSize bool
|
|
||||||
AdminKill bool
|
|
||||||
AdminShuffle bool
|
|
||||||
AdminShuffleToggle bool
|
|
||||||
AdminListSongs bool
|
|
||||||
AdminVersion bool
|
|
||||||
}
|
|
||||||
ServiceKeys struct {
|
|
||||||
Youtube string
|
|
||||||
SoundCloud string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads mumbledj.gcfg into dj.conf, a variable of type DjConfig.
|
|
||||||
func loadConfiguration() error {
|
|
||||||
if gcfg.ReadFileInto(&dj.conf, fmt.Sprintf("%s/.mumbledj/config/mumbledj.gcfg", dj.homeDir)) == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
fmt.Printf("%s/.mumbledj/config/mumbledj.gcfg\n", dj.homeDir)
|
|
||||||
return errors.New("Configuration load failed.")
|
|
||||||
}
|
|
205
service.go
205
service.go
|
@ -1,205 +0,0 @@
|
||||||
/*
|
|
||||||
* MumbleDJ
|
|
||||||
* By Matthieu Grieger
|
|
||||||
* service.go
|
|
||||||
* Copyright (c) 2014, 2015 Matthieu Grieger (MIT License)
|
|
||||||
*/
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/layeh/gumble/gumble"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Service interface. Each service will implement these functions
|
|
||||||
type Service interface {
|
|
||||||
ServiceName() string
|
|
||||||
TrackName() string
|
|
||||||
URLRegex(string) bool
|
|
||||||
NewRequest(*gumble.User, string) ([]Song, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Song interface. Each service will implement these
|
|
||||||
// functions in their Song types.
|
|
||||||
type Song interface {
|
|
||||||
Download() error
|
|
||||||
Play()
|
|
||||||
Delete() error
|
|
||||||
AddSkip(string) error
|
|
||||||
RemoveSkip(string) error
|
|
||||||
SkipReached(int) bool
|
|
||||||
Submitter() string
|
|
||||||
Title() string
|
|
||||||
ID() string
|
|
||||||
Filename() string
|
|
||||||
Duration() time.Duration
|
|
||||||
Thumbnail() string
|
|
||||||
Playlist() Playlist
|
|
||||||
DontSkip() bool
|
|
||||||
SetDontSkip(bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Playlist interface. Each service will implement these
|
|
||||||
// functions in their Playlist types.
|
|
||||||
type Playlist interface {
|
|
||||||
AddSkip(string) error
|
|
||||||
RemoveSkip(string) error
|
|
||||||
DeleteSkippers()
|
|
||||||
SkipReached(int) bool
|
|
||||||
ID() string
|
|
||||||
Title() string
|
|
||||||
}
|
|
||||||
|
|
||||||
var services []Service
|
|
||||||
|
|
||||||
// FindServiceAndAdd tries the given url with each service
|
|
||||||
// and adds the song/playlist with the correct service
|
|
||||||
func FindServiceAndAdd(user *gumble.User, url string) error {
|
|
||||||
var urlService Service
|
|
||||||
|
|
||||||
// Checks all services to see if any can take the URL
|
|
||||||
for _, service := range services {
|
|
||||||
if service.URLRegex(url) {
|
|
||||||
urlService = service
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if urlService == nil {
|
|
||||||
return errors.New(INVALID_URL_MSG)
|
|
||||||
} else {
|
|
||||||
var title string
|
|
||||||
var songsAdded = 0
|
|
||||||
var songArray []Song
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// Get service to create songs
|
|
||||||
if songArray, err = urlService.NewRequest(user, url); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check Playlist Permission
|
|
||||||
if len(songArray) > 1 && !dj.HasPermission(user.Name, dj.conf.Permissions.AdminAddPlaylists) {
|
|
||||||
return errors.New(NO_PLAYLIST_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop through all songs and add to the queue
|
|
||||||
oldLength := dj.queue.Len()
|
|
||||||
for _, song := range songArray {
|
|
||||||
// Check song is not too long
|
|
||||||
if dj.conf.General.MaxSongDuration == 0 || int(song.Duration().Seconds()) <= dj.conf.General.MaxSongDuration {
|
|
||||||
if !isNil(song.Playlist()) {
|
|
||||||
title = song.Playlist().Title()
|
|
||||||
} else {
|
|
||||||
title = song.Title()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add song to queue
|
|
||||||
dj.queue.AddSong(song)
|
|
||||||
songsAdded++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alert channel of added song/playlist
|
|
||||||
if songsAdded == 0 {
|
|
||||||
return errors.New(fmt.Sprintf(TRACK_TOO_LONG_MSG, urlService.ServiceName()))
|
|
||||||
} else if songsAdded == 1 {
|
|
||||||
dj.client.Self.Channel.Send(fmt.Sprintf(SONG_ADDED_HTML, user.Name, title), false)
|
|
||||||
} else {
|
|
||||||
dj.client.Self.Channel.Send(fmt.Sprintf(PLAYLIST_ADDED_HTML, user.Name, title), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Starts playing the new song if nothing else is playing
|
|
||||||
if oldLength == 0 && dj.queue.Len() != 0 && !dj.audioStream.IsPlaying() {
|
|
||||||
if dj.conf.General.AutomaticShuffleOn {
|
|
||||||
dj.queue.RandomNextSong(true)
|
|
||||||
}
|
|
||||||
if err := dj.queue.CurrentSong().Download(); err == nil {
|
|
||||||
dj.queue.CurrentSong().Play()
|
|
||||||
} else {
|
|
||||||
var failMessage = fmt.Sprintf(AUDIO_FAIL_MSG, dj.queue.CurrentSong().Title())
|
|
||||||
dj.queue.CurrentSong().Delete()
|
|
||||||
dj.queue.OnSongFinished()
|
|
||||||
return errors.New(failMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindServiceAndInsertNext tries the given url with each service
|
|
||||||
// and inserts the song/playlist with the correct service into the slot after the current one
|
|
||||||
func FindServiceAndInsertNext(user *gumble.User, url string) error {
|
|
||||||
var urlService Service
|
|
||||||
|
|
||||||
// Checks all services to see if any can take the URL
|
|
||||||
for _, service := range services {
|
|
||||||
if service.URLRegex(url) {
|
|
||||||
urlService = service
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if urlService == nil {
|
|
||||||
return errors.New(INVALID_URL_MSG)
|
|
||||||
} else {
|
|
||||||
var title string
|
|
||||||
var songsAdded = 0
|
|
||||||
var songArray []Song
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// Get service to create songs
|
|
||||||
if songArray, err = urlService.NewRequest(user, url); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check Playlist Permission
|
|
||||||
if len(songArray) > 1 && !dj.HasPermission(user.Name, dj.conf.Permissions.AdminAddPlaylists) {
|
|
||||||
return errors.New(NO_PLAYLIST_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop through all songs and add to the queue
|
|
||||||
i := 0
|
|
||||||
for _, song := range songArray {
|
|
||||||
i++
|
|
||||||
// Check song is not too long
|
|
||||||
if dj.conf.General.MaxSongDuration == 0 || int(song.Duration().Seconds()) <= dj.conf.General.MaxSongDuration {
|
|
||||||
if !isNil(song.Playlist()) {
|
|
||||||
title = song.Playlist().Title()
|
|
||||||
} else {
|
|
||||||
title = song.Title()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add song to queue
|
|
||||||
dj.queue.InsertSong(song, i)
|
|
||||||
songsAdded++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alert channel of added song/playlist
|
|
||||||
if songsAdded == 0 {
|
|
||||||
return errors.New(fmt.Sprintf(TRACK_TOO_LONG_MSG, urlService.ServiceName()))
|
|
||||||
} else if songsAdded == 1 {
|
|
||||||
dj.client.Self.Channel.Send(fmt.Sprintf(NEXT_SONG_ADDED_HTML, user.Name, title), false)
|
|
||||||
} else {
|
|
||||||
dj.client.Self.Channel.Send(fmt.Sprintf(NEXT_PLAYLIST_ADDED_HTML, user.Name, title), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegexpFromURL loops through an array of patterns to see if it matches the url
|
|
||||||
func RegexpFromURL(url string, patterns []string) *regexp.Regexp {
|
|
||||||
for _, pattern := range patterns {
|
|
||||||
if re, err := regexp.Compile(pattern); err == nil {
|
|
||||||
if re.MatchString(url) {
|
|
||||||
return re
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,109 +0,0 @@
|
||||||
/*
|
|
||||||
* MumbleDJ
|
|
||||||
* By Matthieu Grieger
|
|
||||||
* service_mixcloud.go
|
|
||||||
* Copyright (c) 2016 Benjmain Klettbach (MIT License)
|
|
||||||
*/
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/jmoiron/jsonq"
|
|
||||||
"github.com/layeh/gumble/gumble"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Regular expressions for mixcloud urls
|
|
||||||
var mixcloudSongPattern = `https?:\/\/(www\.)?mixcloud\.com\/([\w-]+)\/([\w-]+)(#t=\n\n?(:\n\n)*)?`
|
|
||||||
|
|
||||||
// Mixcloud implements the Service interface
|
|
||||||
type Mixcloud struct{}
|
|
||||||
|
|
||||||
// ------------------
|
|
||||||
// MIXCLOUD SERVICE
|
|
||||||
// ------------------
|
|
||||||
|
|
||||||
// ServiceName is the human readable version of the service name
|
|
||||||
func (mc Mixcloud) ServiceName() string {
|
|
||||||
return "Mixcloud"
|
|
||||||
}
|
|
||||||
|
|
||||||
// TrackName is the human readable version of the service name
|
|
||||||
func (mc Mixcloud) TrackName() string {
|
|
||||||
return "Song"
|
|
||||||
}
|
|
||||||
|
|
||||||
// URLRegex checks to see if service will accept URL
|
|
||||||
func (mc Mixcloud) URLRegex(url string) bool {
|
|
||||||
return RegexpFromURL(url, []string{mixcloudSongPattern}) != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRequest creates the requested song and adds to the queue
|
|
||||||
func (mc Mixcloud) NewRequest(user *gumble.User, url string) ([]Song, error) {
|
|
||||||
var apiResponse *jsonq.JsonQuery
|
|
||||||
var songArray []Song
|
|
||||||
var err error
|
|
||||||
timesplit := strings.Split(url, "#t=")
|
|
||||||
url = strings.Replace(timesplit[0], "www", "api", 1)
|
|
||||||
if apiResponse, err = PerformGetRequest(url); err != nil {
|
|
||||||
return nil, errors.New(INVALID_URL_MSG)
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(url, "playlists") {
|
|
||||||
// PLAYLIST
|
|
||||||
// Playlists from Mixcloud are not supported, because they do not provide an API for them.
|
|
||||||
return nil, errors.New(fmt.Sprintf(NO_PLAYLISTS_SUPPORTED_MSG, mc.ServiceName()))
|
|
||||||
} else {
|
|
||||||
// SONG
|
|
||||||
// Calculate offset
|
|
||||||
offset := 0
|
|
||||||
if len(timesplit) == 2 {
|
|
||||||
timesplit = strings.Split(timesplit[1], ":")
|
|
||||||
multiplier := 1
|
|
||||||
for i := len(timesplit) - 1; i >= 0; i-- {
|
|
||||||
time, _ := strconv.Atoi(timesplit[i])
|
|
||||||
offset += time * multiplier
|
|
||||||
multiplier *= 60
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the track
|
|
||||||
if song, err := mc.NewSong(user, apiResponse, offset); err == nil {
|
|
||||||
return append(songArray, song), err
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSong creates a track and adds to the queue
|
|
||||||
func (mc Mixcloud) NewSong(user *gumble.User, trackData *jsonq.JsonQuery, offset int) (Song, error) {
|
|
||||||
title, _ := trackData.String("name")
|
|
||||||
id, _ := trackData.String("slug")
|
|
||||||
duration, _ := trackData.Int("audio_length")
|
|
||||||
url, _ := trackData.String("url")
|
|
||||||
thumbnail, err := trackData.String("pictures", "large")
|
|
||||||
if err != nil {
|
|
||||||
// Song has no artwork, using profile avatar instead
|
|
||||||
userObj, _ := trackData.Object("user")
|
|
||||||
thumbnail, _ = jsonq.NewQuery(userObj).String("pictures", "thumbnail")
|
|
||||||
}
|
|
||||||
|
|
||||||
song := &AudioTrack{
|
|
||||||
id: id,
|
|
||||||
title: title,
|
|
||||||
url: url,
|
|
||||||
thumbnail: thumbnail,
|
|
||||||
submitter: user,
|
|
||||||
duration: duration,
|
|
||||||
offset: offset,
|
|
||||||
format: "best",
|
|
||||||
skippers: make([]string, 0),
|
|
||||||
dontSkip: false,
|
|
||||||
service: mc,
|
|
||||||
}
|
|
||||||
return song, nil
|
|
||||||
}
|
|
|
@ -1,128 +0,0 @@
|
||||||
/*
|
|
||||||
* MumbleDJ
|
|
||||||
* By Matthieu Grieger
|
|
||||||
* service_soundcloud.go
|
|
||||||
* Copyright (c) 2014, 2015 Matthieu Grieger (MIT License)
|
|
||||||
*/
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/jmoiron/jsonq"
|
|
||||||
"github.com/layeh/gumble/gumble"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Regular expressions for soundcloud urls
|
|
||||||
var soundcloudSongPattern = `https?:\/\/(www\.)?soundcloud\.com\/([\w-]+)\/([\w-]+)(#t=\n\n?(:\n\n)*)?`
|
|
||||||
var soundcloudPlaylistPattern = `https?:\/\/(www\.)?soundcloud\.com\/([\w-]+)\/sets\/([\w-]+)`
|
|
||||||
|
|
||||||
// SoundCloud implements the Service interface
|
|
||||||
type SoundCloud struct{}
|
|
||||||
|
|
||||||
// ------------------
|
|
||||||
// SOUNDCLOUD SERVICE
|
|
||||||
// ------------------
|
|
||||||
|
|
||||||
// ServiceName is the human readable version of the service name
|
|
||||||
func (sc SoundCloud) ServiceName() string {
|
|
||||||
return "Soundcloud"
|
|
||||||
}
|
|
||||||
|
|
||||||
// TrackName is the human readable version of the service name
|
|
||||||
func (sc SoundCloud) TrackName() string {
|
|
||||||
return "Song"
|
|
||||||
}
|
|
||||||
|
|
||||||
// URLRegex checks to see if service will accept URL
|
|
||||||
func (sc SoundCloud) URLRegex(url string) bool {
|
|
||||||
return RegexpFromURL(url, []string{soundcloudSongPattern, soundcloudPlaylistPattern}) != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRequest creates the requested song/playlist and adds to the queue
|
|
||||||
func (sc SoundCloud) NewRequest(user *gumble.User, url string) ([]Song, error) {
|
|
||||||
var apiResponse *jsonq.JsonQuery
|
|
||||||
var songArray []Song
|
|
||||||
var err error
|
|
||||||
timesplit := strings.Split(url, "#t=")
|
|
||||||
url = fmt.Sprintf("http://api.soundcloud.com/resolve?url=%s&client_id=%s", timesplit[0], dj.conf.ServiceKeys.SoundCloud)
|
|
||||||
if apiResponse, err = PerformGetRequest(url); err != nil {
|
|
||||||
return nil, errors.New(fmt.Sprintf(INVALID_API_KEY, sc.ServiceName()))
|
|
||||||
}
|
|
||||||
|
|
||||||
tracks, err := apiResponse.ArrayOfObjects("tracks")
|
|
||||||
if err == nil {
|
|
||||||
// PLAYLIST
|
|
||||||
// Create playlist
|
|
||||||
title, _ := apiResponse.String("title")
|
|
||||||
permalink, _ := apiResponse.String("permalink_url")
|
|
||||||
playlist := &AudioPlaylist{
|
|
||||||
id: permalink,
|
|
||||||
title: title,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dj.conf.General.MaxSongPerPlaylist > 0 && len(tracks) > dj.conf.General.MaxSongPerPlaylist){
|
|
||||||
tracks = tracks[:dj.conf.General.MaxSongPerPlaylist]
|
|
||||||
}
|
|
||||||
// Add all tracks
|
|
||||||
for _, t := range tracks {
|
|
||||||
if song, err := sc.NewSong(user, jsonq.NewQuery(t), 0, playlist); err == nil {
|
|
||||||
songArray = append(songArray, song)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return songArray, nil
|
|
||||||
} else {
|
|
||||||
// SONG
|
|
||||||
// Calculate offset
|
|
||||||
offset := 0
|
|
||||||
if len(timesplit) == 2 {
|
|
||||||
timesplit = strings.Split(timesplit[1], ":")
|
|
||||||
multiplier := 1
|
|
||||||
for i := len(timesplit) - 1; i >= 0; i-- {
|
|
||||||
time, _ := strconv.Atoi(timesplit[i])
|
|
||||||
offset += time * multiplier
|
|
||||||
multiplier *= 60
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the track
|
|
||||||
if song, err := sc.NewSong(user, apiResponse, offset, nil); err == nil {
|
|
||||||
return append(songArray, song), err
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSong creates a track and adds to the queue
|
|
||||||
func (sc SoundCloud) NewSong(user *gumble.User, trackData *jsonq.JsonQuery, offset int, playlist Playlist) (Song, error) {
|
|
||||||
title, _ := trackData.String("title")
|
|
||||||
id, _ := trackData.Int("id")
|
|
||||||
durationMS, _ := trackData.Int("duration")
|
|
||||||
url, _ := trackData.String("permalink_url")
|
|
||||||
thumbnail, err := trackData.String("artwork_url")
|
|
||||||
if err != nil {
|
|
||||||
// Song has no artwork, using profile avatar instead
|
|
||||||
userObj, _ := trackData.Object("user")
|
|
||||||
thumbnail, _ = jsonq.NewQuery(userObj).String("avatar_url")
|
|
||||||
}
|
|
||||||
|
|
||||||
song := &AudioTrack{
|
|
||||||
id: strconv.Itoa(id),
|
|
||||||
title: title,
|
|
||||||
url: url,
|
|
||||||
thumbnail: thumbnail,
|
|
||||||
submitter: user,
|
|
||||||
duration: durationMS / 1000,
|
|
||||||
offset: offset,
|
|
||||||
format: "mp3",
|
|
||||||
playlist: playlist,
|
|
||||||
skippers: make([]string, 0),
|
|
||||||
dontSkip: false,
|
|
||||||
service: sc,
|
|
||||||
}
|
|
||||||
return song, nil
|
|
||||||
}
|
|
|
@ -1,185 +0,0 @@
|
||||||
/*
|
|
||||||
* MumbleDJ
|
|
||||||
* By Matthieu Grieger
|
|
||||||
* service_youtube.go
|
|
||||||
* Copyright (c) 2014, 2015 Matthieu Grieger (MIT License)
|
|
||||||
*/
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
"math"
|
|
||||||
|
|
||||||
"github.com/jmoiron/jsonq"
|
|
||||||
"github.com/layeh/gumble/gumble"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Regular expressions for youtube urls
|
|
||||||
var youtubePlaylistPattern = `https?:\/\/www\.youtube\.com\/playlist\?list=([\w-]+)`
|
|
||||||
var youtubeVideoPatterns = []string{
|
|
||||||
`https?:\/\/www\.youtube\.com\/watch\?v=([\w-]+)(\&t=\d*m?\d*s?)?`,
|
|
||||||
`https?:\/\/youtube\.com\/watch\?v=([\w-]+)(\&t=\d*m?\d*s?)?`,
|
|
||||||
`https?:\/\/youtu.be\/([\w-]+)(\?t=\d*m?\d*s?)?`,
|
|
||||||
`https?:\/\/youtube.com\/v\/([\w-]+)(\?t=\d*m?\d*s?)?`,
|
|
||||||
`https?:\/\/www.youtube.com\/v\/([\w-]+)(\?t=\d*m?\d*s?)?`,
|
|
||||||
}
|
|
||||||
|
|
||||||
// YouTube implements the Service interface
|
|
||||||
type YouTube struct{}
|
|
||||||
|
|
||||||
// ServiceName is the human readable version of the service name
|
|
||||||
func (yt YouTube) ServiceName() string {
|
|
||||||
return "YouTube"
|
|
||||||
}
|
|
||||||
|
|
||||||
// TrackName is the human readable version of the service name
|
|
||||||
func (yt YouTube) TrackName() string {
|
|
||||||
return "Video"
|
|
||||||
}
|
|
||||||
|
|
||||||
// URLRegex checks to see if service will accept URL
|
|
||||||
func (yt YouTube) URLRegex(url string) bool {
|
|
||||||
return RegexpFromURL(url, append(youtubeVideoPatterns, []string{youtubePlaylistPattern}...)) != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRequest creates the requested song/playlist and adds to the queue
|
|
||||||
func (yt YouTube) NewRequest(user *gumble.User, url string) ([]Song, error) {
|
|
||||||
var songArray []Song
|
|
||||||
var shortURL, startOffset = "", ""
|
|
||||||
if re, err := regexp.Compile(youtubePlaylistPattern); err == nil {
|
|
||||||
if re.MatchString(url) {
|
|
||||||
shortURL = re.FindStringSubmatch(url)[1]
|
|
||||||
return yt.NewPlaylist(user, shortURL)
|
|
||||||
} else {
|
|
||||||
re = RegexpFromURL(url, youtubeVideoPatterns)
|
|
||||||
matches := re.FindAllStringSubmatch(url, -1)
|
|
||||||
shortURL = matches[0][1]
|
|
||||||
if len(matches[0]) == 3 {
|
|
||||||
startOffset = matches[0][2]
|
|
||||||
}
|
|
||||||
song, err := yt.NewSong(user, shortURL, startOffset, nil)
|
|
||||||
if !isNil(song) {
|
|
||||||
return append(songArray, song), nil
|
|
||||||
} else {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSong gathers the metadata for a song extracted from a YouTube video, and returns the song.
|
|
||||||
func (yt YouTube) NewSong(user *gumble.User, id, offset string, playlist Playlist) (Song, error) {
|
|
||||||
url := fmt.Sprintf("https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails&id=%s&key=%s", id, dj.conf.ServiceKeys.Youtube)
|
|
||||||
if apiResponse, err := PerformGetRequest(url); err == nil {
|
|
||||||
title, _ := apiResponse.String("items", "0", "snippet", "title")
|
|
||||||
thumbnail, _ := apiResponse.String("items", "0", "snippet", "thumbnails", "high", "url")
|
|
||||||
duration, _ := apiResponse.String("items", "0", "contentDetails", "duration")
|
|
||||||
|
|
||||||
song := &AudioTrack{
|
|
||||||
submitter: user,
|
|
||||||
title: title,
|
|
||||||
id: id,
|
|
||||||
url: "https://youtu.be/" + id,
|
|
||||||
offset: int(yt.parseTime(offset, `T\=(?P<days>\d+D)?(?P<hours>\d+H)?(?P<minutes>\d+M)?(?P<seconds>\d+S)?`).Seconds()),
|
|
||||||
duration: int(yt.parseTime(duration, `P(?P<days>\d+D)?T(?P<hours>\d+H)?(?P<minutes>\d+M)?(?P<seconds>\d+S)?`).Seconds()),
|
|
||||||
thumbnail: thumbnail,
|
|
||||||
format: "bestaudio",
|
|
||||||
skippers: make([]string, 0),
|
|
||||||
playlist: playlist,
|
|
||||||
dontSkip: false,
|
|
||||||
service: yt,
|
|
||||||
}
|
|
||||||
|
|
||||||
return song, nil
|
|
||||||
}
|
|
||||||
return nil, errors.New(fmt.Sprintf(INVALID_API_KEY, yt.ServiceName()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseTime converts from the string youtube returns to a time.Duration
|
|
||||||
func (yt YouTube) parseTime(duration, regex string) time.Duration {
|
|
||||||
var days, hours, minutes, seconds, totalSeconds int64
|
|
||||||
if duration != "" {
|
|
||||||
timestampExp := regexp.MustCompile(regex)
|
|
||||||
timestampMatch := timestampExp.FindStringSubmatch(strings.ToUpper(duration))
|
|
||||||
timestampResult := make(map[string]string)
|
|
||||||
for i, name := range timestampExp.SubexpNames() {
|
|
||||||
if i < len(timestampMatch) {
|
|
||||||
timestampResult[name] = timestampMatch[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if timestampResult["days"] != "" {
|
|
||||||
days, _ = strconv.ParseInt(strings.TrimSuffix(timestampResult["days"], "D"), 10, 32)
|
|
||||||
}
|
|
||||||
if timestampResult["hours"] != "" {
|
|
||||||
hours, _ = strconv.ParseInt(strings.TrimSuffix(timestampResult["hours"], "H"), 10, 32)
|
|
||||||
}
|
|
||||||
if timestampResult["minutes"] != "" {
|
|
||||||
minutes, _ = strconv.ParseInt(strings.TrimSuffix(timestampResult["minutes"], "M"), 10, 32)
|
|
||||||
}
|
|
||||||
if timestampResult["seconds"] != "" {
|
|
||||||
seconds, _ = strconv.ParseInt(strings.TrimSuffix(timestampResult["seconds"], "S"), 10, 32)
|
|
||||||
}
|
|
||||||
|
|
||||||
totalSeconds = int64((days * 86400) + (hours * 3600) + (minutes * 60) + seconds)
|
|
||||||
} else {
|
|
||||||
totalSeconds = 0
|
|
||||||
}
|
|
||||||
output, _ := time.ParseDuration(strconv.Itoa(int(totalSeconds)) + "s")
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPlaylist gathers the metadata for a YouTube playlist and returns it.
|
|
||||||
func (yt YouTube) NewPlaylist(user *gumble.User, id string) ([]Song, error) {
|
|
||||||
var apiResponse *jsonq.JsonQuery
|
|
||||||
var songArray []Song
|
|
||||||
var err error
|
|
||||||
// Retrieve title of playlist
|
|
||||||
url := fmt.Sprintf("https://www.googleapis.com/youtube/v3/playlists?part=snippet&id=%s&key=%s", id, dj.conf.ServiceKeys.Youtube)
|
|
||||||
if apiResponse, err = PerformGetRequest(url); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
title, _ := apiResponse.String("items", "0", "snippet", "title")
|
|
||||||
|
|
||||||
playlist := &AudioPlaylist{
|
|
||||||
id: id,
|
|
||||||
title: title,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
maxSongs := math.MaxInt32
|
|
||||||
if (dj.conf.General.MaxSongPerPlaylist > 0){
|
|
||||||
maxSongs = dj.conf.General.MaxSongPerPlaylist
|
|
||||||
}
|
|
||||||
pageToken := ""
|
|
||||||
for len(songArray) < maxSongs{ //Iterate over the pages
|
|
||||||
|
|
||||||
// Retrieve items in this page of the playlist
|
|
||||||
url = fmt.Sprintf("https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=50&playlistId=%s&key=%s&pageToken=%s",
|
|
||||||
id, dj.conf.ServiceKeys.Youtube, pageToken)
|
|
||||||
if apiResponse, err = PerformGetRequest(url); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
songs, _ := apiResponse.Array("items")
|
|
||||||
for j := 0; j < len(songs) && len(songArray) < maxSongs ; j++ {
|
|
||||||
index := strconv.Itoa(j)
|
|
||||||
videoID, _ := apiResponse.String("items", index, "snippet", "resourceId", "videoId")
|
|
||||||
if song, err := yt.NewSong(user, videoID, "", playlist); err == nil {
|
|
||||||
songArray = append(songArray, song)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if pageToken, err = apiResponse.String("nextPageToken"); err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return songArray, nil
|
|
||||||
}
|
|
88
services/generic_service.go
Normal file
88
services/generic_service.go
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* services/generic_service.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenericService is a generic struct that should be embedded
|
||||||
|
// in other service structs, as it provides useful helper
|
||||||
|
// methods and properties.
|
||||||
|
type GenericService struct {
|
||||||
|
ReadableName string
|
||||||
|
Format string
|
||||||
|
TrackRegex []*regexp.Regexp
|
||||||
|
PlaylistRegex []*regexp.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetReadableName returns the readable name for the service.
|
||||||
|
func (gs *GenericService) GetReadableName() string {
|
||||||
|
return gs.ReadableName
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFormat returns the youtube-dl format for the service.
|
||||||
|
func (gs *GenericService) GetFormat() string {
|
||||||
|
return gs.Format
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckURL matches the passed URL with a list of regex patterns
|
||||||
|
// for valid URLs associated with this service. Returns true if a
|
||||||
|
// match is found, false otherwise.
|
||||||
|
func (gs *GenericService) CheckURL(url string) bool {
|
||||||
|
if gs.isTrack(url) || gs.isPlaylist(url) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gs *GenericService) isTrack(url string) bool {
|
||||||
|
for _, regex := range gs.TrackRegex {
|
||||||
|
if regex.MatchString(url) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gs *GenericService) isPlaylist(url string) bool {
|
||||||
|
for _, regex := range gs.PlaylistRegex {
|
||||||
|
if regex.MatchString(url) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gs *GenericService) getID(url string) (string, error) {
|
||||||
|
var allRegex []*regexp.Regexp
|
||||||
|
|
||||||
|
if gs.PlaylistRegex != nil {
|
||||||
|
allRegex = append(gs.TrackRegex, gs.PlaylistRegex...)
|
||||||
|
} else {
|
||||||
|
allRegex = gs.TrackRegex
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, regex := range allRegex {
|
||||||
|
match := regex.FindStringSubmatch(url)
|
||||||
|
if match == nil {
|
||||||
|
// Move on to next regex, this one didn't match.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result := make(map[string]string)
|
||||||
|
for i, name := range regex.SubexpNames() {
|
||||||
|
if i < len(match) {
|
||||||
|
result[name] = match[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result["id"], nil
|
||||||
|
}
|
||||||
|
return "", errors.New("No match found for URL")
|
||||||
|
}
|
114
services/mixcloud.go
Normal file
114
services/mixcloud.go
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* services/mixcloud.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/antonholmquist/jason"
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/matthieugrieger/mumbledj/bot"
|
||||||
|
"github.com/matthieugrieger/mumbledj/interfaces"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mixcloud is a wrapper around the Mixcloud API.
|
||||||
|
// https://www.mixcloud.com/developers/
|
||||||
|
type Mixcloud struct {
|
||||||
|
*GenericService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMixcloudService returns an initialized Mixcloud service object.
|
||||||
|
func NewMixcloudService() *Mixcloud {
|
||||||
|
return &Mixcloud{
|
||||||
|
&GenericService{
|
||||||
|
ReadableName: "Mixcloud",
|
||||||
|
Format: "m4a",
|
||||||
|
TrackRegex: []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`https?:\/\/(www\.)?mixcloud\.com\/([\w-]+)\/([\w-]+)(#t=\n\n?(:\n\n)*)?`),
|
||||||
|
},
|
||||||
|
// Playlists are currently unsupported by Mixcloud's API.
|
||||||
|
PlaylistRegex: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckAPIKey performs a test API call with the API key
|
||||||
|
// provided in the configuration file to determine if the
|
||||||
|
// service should be enabled.
|
||||||
|
func (mc *Mixcloud) CheckAPIKey() error {
|
||||||
|
// Mixcloud (at the moment) does not require an API key,
|
||||||
|
// so we can just return nil.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTracks uses the passed URL to find and return
|
||||||
|
// tracks associated with the URL. An error is returned
|
||||||
|
// if any error occurs during the API call.
|
||||||
|
func (mc *Mixcloud) GetTracks(url string, submitter *gumble.User) ([]interfaces.Track, error) {
|
||||||
|
var (
|
||||||
|
apiURL string
|
||||||
|
err error
|
||||||
|
resp *http.Response
|
||||||
|
v *jason.Object
|
||||||
|
tracks []interfaces.Track
|
||||||
|
)
|
||||||
|
|
||||||
|
apiURL = strings.Replace(url, "www", "api", 1)
|
||||||
|
|
||||||
|
// Track playback offset is not present in Mixcloud URLs,
|
||||||
|
// so we can safely assume that users will not request
|
||||||
|
// a playback offset in the URL.
|
||||||
|
offset, _ := time.ParseDuration("0s")
|
||||||
|
|
||||||
|
resp, err = http.Get(apiURL)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err = jason.NewObjectFromReader(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, _ := v.GetString("slug")
|
||||||
|
trackURL, _ := v.GetString("url")
|
||||||
|
title, _ := v.GetString("name")
|
||||||
|
author, _ := v.GetString("user", "username")
|
||||||
|
authorURL, _ := v.GetString("user", "url")
|
||||||
|
durationSecs, _ := v.GetInt64("audio_length")
|
||||||
|
duration, _ := time.ParseDuration(fmt.Sprintf("%ds", durationSecs))
|
||||||
|
thumbnail, err := v.GetString("pictures", "large")
|
||||||
|
if err != nil {
|
||||||
|
// Track has no artwork, using profile avatar instead.
|
||||||
|
thumbnail, _ = v.GetString("user", "pictures", "large")
|
||||||
|
}
|
||||||
|
|
||||||
|
track := bot.Track{
|
||||||
|
ID: id,
|
||||||
|
URL: trackURL,
|
||||||
|
Title: title,
|
||||||
|
Author: author,
|
||||||
|
AuthorURL: authorURL,
|
||||||
|
Submitter: submitter.Name,
|
||||||
|
Service: mc.ReadableName,
|
||||||
|
ThumbnailURL: thumbnail,
|
||||||
|
Filename: id + ".track",
|
||||||
|
Duration: duration,
|
||||||
|
PlaybackOffset: offset,
|
||||||
|
Playlist: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
tracks = append(tracks, track)
|
||||||
|
|
||||||
|
return tracks, nil
|
||||||
|
}
|
27
services/pkg_init.go
Normal file
27
services/pkg_init.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* services/pkg_init.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/matthieugrieger/mumbledj/bot"
|
||||||
|
"github.com/matthieugrieger/mumbledj/interfaces"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DJ is an injected MumbleDJ struct.
|
||||||
|
var DJ *bot.MumbleDJ
|
||||||
|
|
||||||
|
// Services is a slice of enabled MumbleDJ services.
|
||||||
|
var Services []interfaces.Service
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Services = []interfaces.Service{
|
||||||
|
NewMixcloudService(),
|
||||||
|
NewSoundCloudService(),
|
||||||
|
NewYouTubeService(),
|
||||||
|
}
|
||||||
|
}
|
191
services/soundcloud.go
Normal file
191
services/soundcloud.go
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* services/soundcloud.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/antonholmquist/jason"
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/matthieugrieger/mumbledj/bot"
|
||||||
|
"github.com/matthieugrieger/mumbledj/interfaces"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SoundCloud is a wrapper around the SoundCloud API.
|
||||||
|
// https://developers.soundcloud.com/docs/api/reference
|
||||||
|
type SoundCloud struct {
|
||||||
|
*GenericService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSoundCloudService returns an initialized SoundCloud service object.
|
||||||
|
func NewSoundCloudService() *SoundCloud {
|
||||||
|
return &SoundCloud{
|
||||||
|
&GenericService{
|
||||||
|
ReadableName: "SoundCloud",
|
||||||
|
Format: "bestaudio",
|
||||||
|
TrackRegex: []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`https?:\/\/(www\.)?soundcloud\.com\/([\w-]+)\/([\w-]+)(#t=\n\n?(:\n\n)*)?`),
|
||||||
|
},
|
||||||
|
PlaylistRegex: []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`https?:\/\/(www\.)?soundcloud\.com\/([\w-]+)\/sets\/([\w-]+)`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckAPIKey performs a test API call with the API key
|
||||||
|
// provided in the configuration file to determine if the
|
||||||
|
// service should be enabled.
|
||||||
|
func (sc *SoundCloud) CheckAPIKey() error {
|
||||||
|
if viper.GetString("api_keys.soundcloud") == "" {
|
||||||
|
return errors.New("No SoundCloud API key has been provided")
|
||||||
|
}
|
||||||
|
url := "http://api.soundcloud.com/tracks/vjflzpbkmerb?client_id=%s"
|
||||||
|
response, err := http.Get(fmt.Sprintf(url, viper.GetString("api.soundcloud")))
|
||||||
|
defer response.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if response.StatusCode != 200 {
|
||||||
|
return errors.New(response.Status)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTracks uses the passed URL to find and return
|
||||||
|
// tracks associated with the URL. An error is returned
|
||||||
|
// if any error occurs during the API call.
|
||||||
|
func (sc *SoundCloud) GetTracks(url string, submitter *gumble.User) ([]interfaces.Track, error) {
|
||||||
|
var (
|
||||||
|
apiURL string
|
||||||
|
err error
|
||||||
|
resp *http.Response
|
||||||
|
v *jason.Object
|
||||||
|
track bot.Track
|
||||||
|
tracks []interfaces.Track
|
||||||
|
)
|
||||||
|
|
||||||
|
urlSplit := strings.Split(url, "#t=")
|
||||||
|
|
||||||
|
apiURL = "http://api.soundcloud.com/resolve?url=%s&client_id=%s"
|
||||||
|
|
||||||
|
if sc.isPlaylist(url) {
|
||||||
|
// Submitter has added a playlist!
|
||||||
|
resp, err = http.Get(fmt.Sprintf(apiURL, urlSplit[0], viper.GetString("api_keys.soundcloud")))
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err = jason.NewObjectFromReader(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
title, _ := v.GetString("title")
|
||||||
|
permalink, _ := v.GetString("permalink_url")
|
||||||
|
playlist := &bot.Playlist{
|
||||||
|
ID: permalink,
|
||||||
|
Title: title,
|
||||||
|
Submitter: submitter.Name,
|
||||||
|
Service: sc.ReadableName,
|
||||||
|
}
|
||||||
|
|
||||||
|
var scTracks []*jason.Object
|
||||||
|
scTracks, err = v.GetObjectArray("tracks")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dummyOffset, _ := time.ParseDuration("0s")
|
||||||
|
for _, t := range scTracks {
|
||||||
|
track, err = sc.getTrack(t, dummyOffset, submitter)
|
||||||
|
if err != nil {
|
||||||
|
// Skip this track.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
track.Playlist = playlist
|
||||||
|
tracks = append(tracks, track)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tracks) == 0 {
|
||||||
|
return nil, errors.New("Invalid playlist. No tracks were added")
|
||||||
|
}
|
||||||
|
return tracks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submitter has added a track!
|
||||||
|
|
||||||
|
offset := 0
|
||||||
|
// Calculate track offset if needed
|
||||||
|
if len(urlSplit) == 2 {
|
||||||
|
timeSplit := strings.Split(urlSplit[1], ":")
|
||||||
|
multiplier := 1
|
||||||
|
for i := len(timeSplit) - 1; i >= 0; i-- {
|
||||||
|
time, _ := strconv.Atoi(timeSplit[i])
|
||||||
|
offset += time * multiplier
|
||||||
|
multiplier *= 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
playbackOffset, _ := time.ParseDuration(fmt.Sprintf("%ds", offset))
|
||||||
|
|
||||||
|
resp, err = http.Get(fmt.Sprintf(apiURL, urlSplit[0], viper.GetString("api_keys.soundcloud")))
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err = jason.NewObjectFromReader(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
track, err = sc.getTrack(v, playbackOffset, submitter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tracks = append(tracks, track)
|
||||||
|
return tracks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *SoundCloud) getTrack(obj *jason.Object, offset time.Duration, submitter *gumble.User) (bot.Track, error) {
|
||||||
|
title, _ := obj.GetString("title")
|
||||||
|
id, _ := obj.GetString("id")
|
||||||
|
url, _ := obj.GetString("permalink_url")
|
||||||
|
author, _ := obj.GetString("user", "username")
|
||||||
|
authorURL, _ := obj.GetString("user", "permalink_url")
|
||||||
|
durationMS, _ := obj.GetInt64("duration")
|
||||||
|
duration, _ := time.ParseDuration(fmt.Sprintf("%dms", durationMS))
|
||||||
|
thumbnail, err := obj.GetString("artwork_url")
|
||||||
|
if err != nil {
|
||||||
|
// Track has no artwork, using profile avatar instead.
|
||||||
|
thumbnail, _ = obj.GetString("user", "avatar_url")
|
||||||
|
}
|
||||||
|
|
||||||
|
return bot.Track{
|
||||||
|
ID: id,
|
||||||
|
URL: url,
|
||||||
|
Title: title,
|
||||||
|
Author: author,
|
||||||
|
AuthorURL: authorURL,
|
||||||
|
Submitter: submitter.Name,
|
||||||
|
Service: sc.ReadableName,
|
||||||
|
Filename: id + ".track",
|
||||||
|
ThumbnailURL: thumbnail,
|
||||||
|
Duration: duration,
|
||||||
|
PlaybackOffset: offset,
|
||||||
|
Playlist: nil,
|
||||||
|
}, nil
|
||||||
|
}
|
221
services/youtube.go
Normal file
221
services/youtube.go
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
/*
|
||||||
|
* MumbleDJ
|
||||||
|
* By Matthieu Grieger
|
||||||
|
* services/youtube.go
|
||||||
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/ChannelMeter/iso8601duration"
|
||||||
|
"github.com/antonholmquist/jason"
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
"github.com/matthieugrieger/mumbledj/bot"
|
||||||
|
"github.com/matthieugrieger/mumbledj/interfaces"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// YouTube is a wrapper around the YouTube Data API.
|
||||||
|
// https://developers.google.com/youtube/v3/docs/
|
||||||
|
type YouTube struct {
|
||||||
|
*GenericService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewYouTubeService returns an initialized YouTube service object.
|
||||||
|
func NewYouTubeService() *YouTube {
|
||||||
|
return &YouTube{
|
||||||
|
&GenericService{
|
||||||
|
ReadableName: "YouTube",
|
||||||
|
Format: "bestaudio",
|
||||||
|
TrackRegex: []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`https?:\/\/www.youtube.com\/watch\?v=(?P<id>[\w-]+)(?P<timestamp>\&t=\d*m?\d*s?)?`),
|
||||||
|
regexp.MustCompile(`https?:\/\/youtube.com\/watch\?v=(?P<id>[\w-]+)(?P<timestamp>\&t=\d*m?\d*s?)?`),
|
||||||
|
regexp.MustCompile(`https?:\/\/youtu.be\/(?P<id>[\w-]+)(?P<timestamp>\?t=\d*m?\d*s?)?`),
|
||||||
|
regexp.MustCompile(`https?:\/\/youtube.com\/v\/(?P<id>[\w-]+)(?P<timestamp>\?t=\d*m?\d*s?)?`),
|
||||||
|
regexp.MustCompile(`https?:\/\/www.youtube.com\/v\/(?P<id>[\w-]+)(?P<timestamp>\?t=\d*m?\d*s?)?`),
|
||||||
|
},
|
||||||
|
PlaylistRegex: []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`https?:\/\/www\.youtube\.com\/playlist\?list=(?P<id>[\w-]+)`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckAPIKey performs a test API call with the API key
|
||||||
|
// provided in the configuration file to determine if the
|
||||||
|
// service should be enabled.
|
||||||
|
func (yt *YouTube) CheckAPIKey() error {
|
||||||
|
var (
|
||||||
|
response *http.Response
|
||||||
|
v *jason.Object
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if viper.GetString("api_keys.youtube") == "" {
|
||||||
|
return errors.New("No YouTube API key has been provided")
|
||||||
|
}
|
||||||
|
url := "https://www.googleapis.com/youtube/v3/videos?part=snippet&id=KQY9zrjPBjo&key=%s"
|
||||||
|
response, err = http.Get(fmt.Sprintf(url, viper.GetString("api_keys.youtube")))
|
||||||
|
defer response.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err = jason.NewObjectFromReader(response.Body); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err = v.GetObject("error"); err == nil {
|
||||||
|
message, _ := v.GetString("message")
|
||||||
|
code, _ := v.GetInt64("code")
|
||||||
|
errArray, _ := v.GetObjectArray("errors")
|
||||||
|
reason, _ := errArray[0].GetString("reason")
|
||||||
|
|
||||||
|
return fmt.Errorf("%d: %s (reason: %s)", code, message, reason)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTracks uses the passed URL to find and return
|
||||||
|
// tracks associated with the URL. An error is returned
|
||||||
|
// if any error occurs during the API call.
|
||||||
|
func (yt *YouTube) GetTracks(url string, submitter *gumble.User) ([]interfaces.Track, error) {
|
||||||
|
var (
|
||||||
|
playlistURL string
|
||||||
|
playlistItemsURL string
|
||||||
|
id string
|
||||||
|
err error
|
||||||
|
resp *http.Response
|
||||||
|
v *jason.Object
|
||||||
|
track bot.Track
|
||||||
|
tracks []interfaces.Track
|
||||||
|
)
|
||||||
|
|
||||||
|
playlistURL = "https://www.googleapis.com/youtube/v3/playlists?part=snippet&id=%s&key=%s"
|
||||||
|
playlistItemsURL = "https://www.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails&playlistId=%s&maxResults=%d&key=%s&pageToken=%s"
|
||||||
|
id, err = yt.getID(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if yt.isPlaylist(url) {
|
||||||
|
resp, err = http.Get(fmt.Sprintf(playlistURL, id, viper.GetString("api_keys.youtube")))
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err = jason.NewObjectFromReader(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
items, _ := v.GetObjectArray("items")
|
||||||
|
item := items[0]
|
||||||
|
|
||||||
|
title, _ := item.GetString("snippet", "title")
|
||||||
|
|
||||||
|
playlist := &bot.Playlist{
|
||||||
|
ID: id,
|
||||||
|
Title: title,
|
||||||
|
Submitter: submitter.Name,
|
||||||
|
Service: yt.ReadableName,
|
||||||
|
}
|
||||||
|
|
||||||
|
maxItems := math.MaxInt32
|
||||||
|
if viper.GetInt("queue.max_tracks_per_playlist") > 0 {
|
||||||
|
maxItems = viper.GetInt("queue.max_tracks_per_playlist")
|
||||||
|
}
|
||||||
|
|
||||||
|
pageToken := ""
|
||||||
|
for len(tracks) < maxItems {
|
||||||
|
curResp, curErr := http.Get(fmt.Sprintf(playlistItemsURL, id, maxItems, viper.GetString("api_keys.youtube"), pageToken))
|
||||||
|
defer curResp.Body.Close()
|
||||||
|
if curErr != nil {
|
||||||
|
// An error occurred, simply skip this track.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err = jason.NewObjectFromReader(curResp.Body)
|
||||||
|
if err != nil {
|
||||||
|
// An error occurred, simply skip this track.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
curTracks, _ := v.GetObjectArray("items")
|
||||||
|
for _, track := range curTracks {
|
||||||
|
videoID, _ := track.GetString("snippet", "resourceId", "videoId")
|
||||||
|
|
||||||
|
// Unfortunately we have to execute another API call for each video as the YouTube API does not
|
||||||
|
// return video durations from the playlistItems endpoint...
|
||||||
|
newTrack, _ := yt.getTrack(videoID, submitter)
|
||||||
|
newTrack.Playlist = playlist
|
||||||
|
tracks = append(tracks, newTrack)
|
||||||
|
|
||||||
|
if len(tracks) >= maxItems {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tracks) == 0 {
|
||||||
|
return nil, errors.New("Invalid playlist. No tracks were added")
|
||||||
|
}
|
||||||
|
return tracks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
track, err = yt.getTrack(id, submitter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tracks = append(tracks, track)
|
||||||
|
return tracks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (yt *YouTube) getTrack(id string, submitter *gumble.User) (bot.Track, error) {
|
||||||
|
var (
|
||||||
|
resp *http.Response
|
||||||
|
err error
|
||||||
|
v *jason.Object
|
||||||
|
)
|
||||||
|
|
||||||
|
videoURL := "https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails&id=%s&key=%s"
|
||||||
|
resp, err = http.Get(fmt.Sprintf(videoURL, id, viper.GetString("api_keys.youtube")))
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return bot.Track{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err = jason.NewObjectFromReader(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return bot.Track{}, err
|
||||||
|
}
|
||||||
|
items, _ := v.GetObjectArray("items")
|
||||||
|
item := items[0]
|
||||||
|
title, _ := item.GetString("snippet", "title")
|
||||||
|
thumbnail, _ := item.GetString("snippet", "thumbnails", "high", "url")
|
||||||
|
author, _ := item.GetString("snippet", "channelTitle")
|
||||||
|
durationString, _ := item.GetString("contentDetails", "duration")
|
||||||
|
durationConverted, _ := duration.FromString(durationString)
|
||||||
|
duration := durationConverted.ToDuration()
|
||||||
|
|
||||||
|
return bot.Track{
|
||||||
|
ID: id,
|
||||||
|
URL: "https://youtube.com/watch?v=" + id,
|
||||||
|
Title: title,
|
||||||
|
Author: author,
|
||||||
|
Submitter: submitter.Name,
|
||||||
|
Service: yt.ReadableName,
|
||||||
|
Filename: id + ".track",
|
||||||
|
ThumbnailURL: thumbnail,
|
||||||
|
Duration: duration,
|
||||||
|
Playlist: nil,
|
||||||
|
}, nil
|
||||||
|
}
|
145
songqueue.go
145
songqueue.go
|
@ -1,145 +0,0 @@
|
||||||
/*
|
|
||||||
* MumbleDJ
|
|
||||||
* By Matthieu Grieger
|
|
||||||
* songqueue.go
|
|
||||||
* Copyright (c) 2014, 2015 Matthieu Grieger (MIT License)
|
|
||||||
*/
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rand.Seed(time.Now().UTC().UnixNano())
|
|
||||||
}
|
|
||||||
|
|
||||||
// SongQueue type declaration.
|
|
||||||
type SongQueue struct {
|
|
||||||
queue []Song
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSongQueue initializes a new queue and returns it.
|
|
||||||
func NewSongQueue() *SongQueue {
|
|
||||||
return &SongQueue{
|
|
||||||
queue: make([]Song, 0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddSong adds a Song to the SongQueue.
|
|
||||||
func (q *SongQueue) AddSong(s Song) error {
|
|
||||||
beforeLen := q.Len()
|
|
||||||
q.queue = append(q.queue, s)
|
|
||||||
if len(q.queue) == beforeLen+1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return errors.New("Could not add Song to the SongQueue.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertSong inserts a Song to the SongQueue at a location.
|
|
||||||
func (q *SongQueue) InsertSong(s Song, i int) error {
|
|
||||||
beforeLen := q.Len()
|
|
||||||
q.queue = append(q.queue[:i], append([]Song{s}, q.queue[i:]...)...)
|
|
||||||
if len(q.queue) == beforeLen+1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return errors.New("Could not insert Song to the SongQueue.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// CurrentSong returns the current Song.
|
|
||||||
func (q *SongQueue) CurrentSong() Song {
|
|
||||||
return q.queue[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// NextSong moves to the next Song in SongQueue. NextSong() removes the first Song in the queue.
|
|
||||||
func (q *SongQueue) NextSong() {
|
|
||||||
if !isNil(q.CurrentSong().Playlist()) {
|
|
||||||
if s, err := q.PeekNext(); err == nil {
|
|
||||||
if !isNil(s.Playlist()) {
|
|
||||||
if q.CurrentSong().Playlist().ID() != s.Playlist().ID() {
|
|
||||||
q.CurrentSong().Playlist().DeleteSkippers()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
q.CurrentSong().Playlist().DeleteSkippers()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
q.queue = q.queue[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// PeekNext peeks at the next Song and returns it.
|
|
||||||
func (q *SongQueue) PeekNext() (Song, error) {
|
|
||||||
if q.Len() > 1 {
|
|
||||||
if dj.conf.General.AutomaticShuffleOn { //Shuffle mode is active
|
|
||||||
q.RandomNextSong(false)
|
|
||||||
}
|
|
||||||
return q.queue[1], nil
|
|
||||||
}
|
|
||||||
return nil, errors.New("There isn't a Song coming up next.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len returns the length of the SongQueue.
|
|
||||||
func (q *SongQueue) Len() int {
|
|
||||||
return len(q.queue)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Traverse is a traversal function for SongQueue. Allows a visit function to be passed in which performs
|
|
||||||
// the specified action on each queue item.
|
|
||||||
func (q *SongQueue) Traverse(visit func(i int, s Song)) {
|
|
||||||
for sQueue, queueSong := range q.queue {
|
|
||||||
visit(sQueue, queueSong)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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))
|
|
||||||
dj.audioStream.Offset = resetOffset
|
|
||||||
if q.Len() != 0 {
|
|
||||||
if dj.queue.CurrentSong().DontSkip() == true {
|
|
||||||
dj.queue.CurrentSong().SetDontSkip(false)
|
|
||||||
q.PrepareAndPlayNextSong()
|
|
||||||
} else {
|
|
||||||
q.NextSong()
|
|
||||||
if q.Len() != 0 {
|
|
||||||
q.PrepareAndPlayNextSong()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrepareAndPlayNextSong prepares next song and plays it if the download succeeds.
|
|
||||||
// Otherwise the function will print an error message to the channel and skip to the next song.
|
|
||||||
func (q *SongQueue) PrepareAndPlayNextSong() {
|
|
||||||
if err := q.CurrentSong().Download(); err == nil {
|
|
||||||
q.CurrentSong().Play()
|
|
||||||
} else {
|
|
||||||
dj.client.Self.Channel.Send(fmt.Sprintf(AUDIO_FAIL_MSG, q.CurrentSong().Title()), false)
|
|
||||||
q.OnSongFinished()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shuffles the songqueue using inside-out algorithm
|
|
||||||
func (q *SongQueue) ShuffleSongs() {
|
|
||||||
for i := range q.queue[1:] { //Don't touch currently playing song
|
|
||||||
j := rand.Intn(i + 1)
|
|
||||||
q.queue[i+1], q.queue[j+1] = q.queue[j+1], q.queue[i+1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sets a random song as next song to be played
|
|
||||||
// queueWasEmpty wether the queue was empty before adding the last song
|
|
||||||
func (q *SongQueue) RandomNextSong(queueWasEmpty bool) {
|
|
||||||
if q.Len() > 1 {
|
|
||||||
nextSongIndex := 1
|
|
||||||
if queueWasEmpty {
|
|
||||||
nextSongIndex = 0
|
|
||||||
}
|
|
||||||
swapIndex := nextSongIndex + rand.Intn(q.Len()-1)
|
|
||||||
q.queue[nextSongIndex], q.queue[swapIndex] = q.queue[swapIndex], q.queue[nextSongIndex]
|
|
||||||
}
|
|
||||||
}
|
|
224
strings.go
224
strings.go
|
@ -1,224 +0,0 @@
|
||||||
/*
|
|
||||||
* MumbleDJ
|
|
||||||
* By Matthieu Grieger
|
|
||||||
* strings.go
|
|
||||||
* Copyright (c) 2014, 2015 Matthieu Grieger (MIT License)
|
|
||||||
*/
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
// Current version of the bot
|
|
||||||
const VERSION = "v2.10.0"
|
|
||||||
|
|
||||||
// Message shown to users when they request the version of the bot
|
|
||||||
const DJ_VERSION = "MumbleDJ <b>" + VERSION + "</b>"
|
|
||||||
|
|
||||||
// Message shown to users when the bot has an invalid API key.
|
|
||||||
const INVALID_API_KEY = "MumbleDJ does not have a valid %s API key."
|
|
||||||
|
|
||||||
// Message shown to users when they do not have permission to execute a command.
|
|
||||||
const NO_PERMISSION_MSG = "You do not have permission to execute that command."
|
|
||||||
|
|
||||||
// Message shown to users when they try to add a playlist to the queue and do not have permission to do so.
|
|
||||||
const NO_PLAYLIST_PERMISSION_MSG = "You do not have permission to add playlists to the queue."
|
|
||||||
|
|
||||||
// Message shown to users when they try to execute a command that doesn't exist.
|
|
||||||
const COMMAND_DOESNT_EXIST_MSG = "The command you entered does not exist."
|
|
||||||
|
|
||||||
// Message shown to users when they try to move the bot to a non-existant channel.
|
|
||||||
const CHANNEL_DOES_NOT_EXIST_MSG = "The channel you specified does not exist."
|
|
||||||
|
|
||||||
// Message shown to users when they attempt to add an invalid URL to the queue.
|
|
||||||
const INVALID_URL_MSG = "The URL you submitted does not match the required format."
|
|
||||||
|
|
||||||
// Message shown to users when they attempt to add a video that's too long
|
|
||||||
const TRACK_TOO_LONG_MSG = "The %s you submitted exceeds the duration allowed by the server."
|
|
||||||
|
|
||||||
// Message shown to users when they attempt to perform an action on a song when
|
|
||||||
// no song is playing.
|
|
||||||
const NO_MUSIC_PLAYING_MSG = "There is no music playing at the moment."
|
|
||||||
|
|
||||||
// Message shown to users when they attempt to skip a playlist when there is no playlist playing.
|
|
||||||
const NO_PLAYLIST_PLAYING_MSG = "There is no playlist playing at the moment."
|
|
||||||
|
|
||||||
// Message shown to users when they try to play a playlist from a source which doesn't support playlists.
|
|
||||||
const NO_PLAYLISTS_SUPPORTED_MSG = "Playlists from %s are not supported."
|
|
||||||
|
|
||||||
// Message shown to users when they attempt to use the nextsong command when there is no song coming up.
|
|
||||||
const NO_SONG_NEXT_MSG = "There are no songs queued at the moment."
|
|
||||||
|
|
||||||
// Message shown to users when they issue a command that requires an argument and one was not supplied.
|
|
||||||
const NO_ARGUMENT_MSG = "The command you issued requires an argument and you did not provide one."
|
|
||||||
|
|
||||||
// Message shown to users when they try to change the volume to a value outside the volume range.
|
|
||||||
const NOT_IN_VOLUME_RANGE_MSG = "Out of range. The volume must be between %f and %f."
|
|
||||||
|
|
||||||
// Message shown to user when a successful configuration reload finishes.
|
|
||||||
const CONFIG_RELOAD_SUCCESS_MSG = "The configuration has been successfully reloaded."
|
|
||||||
|
|
||||||
// Message shown to users when an admin skips a song.
|
|
||||||
const ADMIN_SONG_SKIP_MSG = "An admin has decided to skip the current song."
|
|
||||||
|
|
||||||
// Message shown to users when an admin skips a playlist.
|
|
||||||
const ADMIN_PLAYLIST_SKIP_MSG = "An admin has decided to skip the current playlist."
|
|
||||||
|
|
||||||
// Message shown to users when the audio for a video could not be downloaded.
|
|
||||||
const AUDIO_FAIL_MSG = "The audio download for this video failed. <b>%s</b> has likely not generated the audio files for this track yet. Skipping to the next song!"
|
|
||||||
|
|
||||||
// Message shown to users when they supply an URL that does not contain a valid ID.
|
|
||||||
const INVALID_ID_MSG = "The %s URL you supplied did not contain a valid ID."
|
|
||||||
|
|
||||||
// Message shown to user when they successfully update the bot's comment.
|
|
||||||
const COMMENT_UPDATED_MSG = "The comment for the bot has successfully been updated."
|
|
||||||
|
|
||||||
// Message shown to user when they request to see the number of songs cached on disk.
|
|
||||||
const NUM_CACHED_MSG = "There are currently %d songs cached on disk."
|
|
||||||
|
|
||||||
// Message shown to user when they request to see the total size of the cache.
|
|
||||||
const CACHE_SIZE_MSG = "The cache is currently %g MB in size."
|
|
||||||
|
|
||||||
// Message shown to user when they attempt to issue a cache-related command when caching is not enabled.
|
|
||||||
const CACHE_NOT_ENABLED_MSG = "The cache is not currently enabled."
|
|
||||||
|
|
||||||
// Message shown to user when they attempt to shuffle the queue and it has less than 2 elements.
|
|
||||||
const CANT_SHUFFLE_MSG = "Can't shuffle the queue if there is less than 2 songs."
|
|
||||||
|
|
||||||
// Message shown to users when the songqueue has been successfully shuffled.
|
|
||||||
const SHUFFLE_SUCCESS_MSG = "The current songqueue has been successfully shuffled by <b>%s</b> (starting from next song)."
|
|
||||||
|
|
||||||
// Message shown to users when automatic shuffle is activated
|
|
||||||
const SHUFFLE_ON_MESSAGE = "<b>%s</b> has turned automatic shuffle on."
|
|
||||||
|
|
||||||
// Message shown to users when automatic shuffle is deactivated
|
|
||||||
const SHUFFLE_OFF_MESSAGE = "<b>%s</b> has turned automatic shuffle off."
|
|
||||||
|
|
||||||
// Message shown to user when they attempt to enable automatic shuffle while it's already activated
|
|
||||||
const SHUFFLE_ACTIVATED_ERROR_MESSAGE = "Automatic shuffle is already activated."
|
|
||||||
|
|
||||||
// Message shown to user when they attempt to disable automatic shuffle while it's already deactivated
|
|
||||||
const SHUFFLE_DEACTIVATED_ERROR_MESSAGE = "Automatic shuffle is already deactivated."
|
|
||||||
|
|
||||||
// Message shown to user when they attempt to move the bot and it is already playing audio to others.
|
|
||||||
const PEOPLE_ARE_LISTENING_TO_ME = "Users in another channel are listening to me."
|
|
||||||
|
|
||||||
// Message shown to channel when a song is added to the queue by a user.
|
|
||||||
const SONG_ADDED_HTML = `
|
|
||||||
<b>%s</b> has added "%s" to the queue.
|
|
||||||
`
|
|
||||||
|
|
||||||
// Message shown to channel when a playlist is added to the queue by a user.
|
|
||||||
const PLAYLIST_ADDED_HTML = `
|
|
||||||
<b>%s</b> has added the playlist "%s" to the queue.
|
|
||||||
`
|
|
||||||
|
|
||||||
// Message shown to channel when a song is added to the queue by a user after the current song.
|
|
||||||
const NEXT_SONG_ADDED_HTML = `
|
|
||||||
<b>%s</b> has added "%s" to the queue after the current song.
|
|
||||||
`
|
|
||||||
|
|
||||||
// Message shown to channel when a playlist is added to the queue by a user after the current song.
|
|
||||||
const NEXT_PLAYLIST_ADDED_HTML = `
|
|
||||||
<b>%s</b> has added the playlist "%s" to the queue after the current song.
|
|
||||||
`
|
|
||||||
|
|
||||||
// Message shown to channel when a song has been skipped.
|
|
||||||
const SONG_SKIPPED_HTML = `
|
|
||||||
The number of votes required for a skip has been met. <b>Skipping song!</b>
|
|
||||||
`
|
|
||||||
|
|
||||||
// Message shown to channel when a playlist has been skipped.
|
|
||||||
const PLAYLIST_SKIPPED_HTML = `
|
|
||||||
The number of votes required for a skip has been met. <b>Skipping playlist!</b>
|
|
||||||
`
|
|
||||||
|
|
||||||
// Message shown to display bot commands.
|
|
||||||
const HELP_HTML = `<br/>
|
|
||||||
<b>User Commands:</b>
|
|
||||||
<p><b>!help</b> - Displays this help.</p>
|
|
||||||
<p><b>!add</b> - Adds songs/playlists to queue.</p>
|
|
||||||
<p><b>!volume</b> - Either tells you the current volume or sets it to a new volume.</p>
|
|
||||||
<p><b>!skip</b> - Casts a vote to skip the current song</p>
|
|
||||||
<p> <b>!skipplaylist</b> - Casts a vote to skip over the current playlist.</p>
|
|
||||||
<p><b>!numsongs</b> - Shows how many songs are in queue.</p>
|
|
||||||
<p><b>!listsongs</b> - Lists the songs in queue.</p>
|
|
||||||
<p><b>!nextsong</b> - Shows the title and submitter of the next queue item if it exists.</p>
|
|
||||||
<p><b>!currentsong</b> - Shows the title and submitter of the song currently playing.</p>
|
|
||||||
<p><b>!version</b> - Shows the version of the bot.</p>
|
|
||||||
<p style="-qt-paragraph-type:empty"><br/></p>
|
|
||||||
<p><b>Admin Commands:</b></p>
|
|
||||||
<p><b>!addnext</b> - Adds songs/playlists to queue after the current song.</p>
|
|
||||||
<p><b>!reset</b> - An admin command that resets the song queue. </p>
|
|
||||||
<p><b>!forceskip</b> - An admin command that forces a song skip. </p>
|
|
||||||
<p><b>!forceskipplaylist</b> - An admin command that forces a playlist skip. </p>
|
|
||||||
<p><b>!shuffle</b> - An admin command that shuffles the current queue. </p>
|
|
||||||
<p><b>!shuffleon</b> - An admin command that enables auto shuffling.</p>
|
|
||||||
<p><b>!shuffleoff</b> - An admin command that disables auto shuffling.</p>
|
|
||||||
<p><b>!move </b>- Moves MumbleDJ into channel if it exists.</p>
|
|
||||||
<p><b>!joinme </b>- Moves MumbleDJ into your current channel if not playing audio to someone else.</p>
|
|
||||||
<p><b>!reload</b> - Reloads mumbledj.gcfg configuration settings.</p>
|
|
||||||
<p><b>!setcomment</b> - Sets the comment for the bot.</p>
|
|
||||||
<p><b>!numcached</b></p> - Outputs the number of songs cached on disk.</p>
|
|
||||||
<p><b>!cachesize</b></p> - Outputs the total file size of the cache in MB.</p>
|
|
||||||
<p><b>!kill</b> - Safely cleans the bot environment and disconnects from the server.</p>
|
|
||||||
`
|
|
||||||
|
|
||||||
// Message shown to users when they ask for the current volume (volume command without argument)
|
|
||||||
const CUR_VOLUME_HTML = `
|
|
||||||
The current volume is <b>%.2f</b>.
|
|
||||||
`
|
|
||||||
|
|
||||||
// Message shown to users when another user votes to skip the current song.
|
|
||||||
const SKIP_ADDED_HTML = `
|
|
||||||
<b>%s</b> has voted to skip the current song.
|
|
||||||
`
|
|
||||||
|
|
||||||
// Message shown to users when the submitter of a song decides to skip their song.
|
|
||||||
const SUBMITTER_SKIP_HTML = `
|
|
||||||
The current song has been skipped by <b>%s</b>, the submitter.
|
|
||||||
`
|
|
||||||
|
|
||||||
// Message shown to users when another user votes to skip the current playlist.
|
|
||||||
const PLAYLIST_SKIP_ADDED_HTML = `
|
|
||||||
<b>%s</b> has voted to skip the current playlist.
|
|
||||||
`
|
|
||||||
|
|
||||||
// Message shown to users when the submitter of a song decides to skip their song.
|
|
||||||
const PLAYLIST_SUBMITTER_SKIP_HTML = `
|
|
||||||
The current playlist has been skipped by <b>%s</b>, the submitter.
|
|
||||||
`
|
|
||||||
|
|
||||||
// Message shown to users when they successfully change the volume.
|
|
||||||
const VOLUME_SUCCESS_HTML = `
|
|
||||||
<b>%s</b> has changed the volume to <b>%.2f</b>.
|
|
||||||
`
|
|
||||||
|
|
||||||
// Message shown to users when a user successfully resets the SongQueue.
|
|
||||||
const QUEUE_RESET_HTML = `
|
|
||||||
<b>%s</b> has cleared the song queue.
|
|
||||||
`
|
|
||||||
|
|
||||||
// Message shown to users when a user asks how many songs are in the queue.
|
|
||||||
const NUM_SONGS_HTML = `
|
|
||||||
There are currently <b>%d</b> song(s) in the queue.
|
|
||||||
`
|
|
||||||
|
|
||||||
// Message shown to users when they issue the nextsong command.
|
|
||||||
const NEXT_SONG_HTML = `
|
|
||||||
The next song in the queue is "%s", added by <b>%s</b>.
|
|
||||||
`
|
|
||||||
|
|
||||||
// Message shown to users when they issue the currentsong command.
|
|
||||||
const CURRENT_SONG_HTML = `
|
|
||||||
The song currently playing is "%s", added by <b>%s</b>.
|
|
||||||
`
|
|
||||||
|
|
||||||
// Message shown to users when the currentsong command is issued when a song from a
|
|
||||||
// playlist is playing.
|
|
||||||
const CURRENT_SONG_PLAYLIST_HTML = `
|
|
||||||
The song currently playing is "%s", added <b>%s</b> from the playlist "%s".
|
|
||||||
`
|
|
||||||
|
|
||||||
// Message shown to user when the listsongs command is issued
|
|
||||||
const SONG_LIST_HTML = `
|
|
||||||
<br>%d: "%s", added by <b>%s</b>.</br>
|
|
||||||
`
|
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue