Soundcloud links with a time will be offset

This commit is contained in:
MichaelOultram 2015-08-16 02:04:36 +01:00
parent 861c3bba31
commit edba60939a
14 changed files with 56 additions and 196 deletions

1
.gitattributes vendored
View file

@ -1 +0,0 @@
README.md merge=ours

View file

@ -1,8 +0,0 @@
code.google.com/p/gcfg #c2d3050044d0
github.com/golang/protobuf #0f7a9caded1fb3c9cc5a9b4bcf2ff633cc8ae644
github.com/jmoiron/jsonq #7c27c8eb9f6831555a4209f6a7d579159e766a3c
github.com/layeh/gopus #2f86fa22bc209cc0ccbc6418dfbad9199e3dbc78
github.com/layeh/gumble/gumble #8b9989d9c4090874546c45ceaa6ff21e95705bc4
github.com/layeh/gumble/gumble_ffmpeg #c9fcce8fc4b71c7c53a5d3d9d48a1e001ad19a19
github.com/layeh/gumble/gumbleutil #abf58b0ea8b2661897f81cf69c2a6a3e37152d74
github.com/timshannon/go-openal #f4fbb66b2922de93753ac8069ff62d20a56a7450

View file

@ -1,7 +1,7 @@
all: mumbledj all: mumbledj
mumbledj: main.go commands.go parseconfig.go strings.go service.go youtube_dl.go service_youtube.go service_soundcloud.go songqueue.go cache.go mumbledj: main.go commands.go parseconfig.go strings.go service.go youtube_dl.go service_youtube.go service_soundcloud.go songqueue.go cache.go
if [ ! -f $(GOPATH)/bin/goop ]; then go get github.com/nitrous-io/goop; fi; go get github.com/nitrous-io/goop
rm -rf Goopfile.lock rm -rf Goopfile.lock
goop install goop install
goop go build goop go build

View file

@ -1,6 +1,6 @@
MumbleDJ [![Build Status](https://travis-ci.org/MichaelOultram/mumbledj.svg?branch=master)](https://travis-ci.org/MichaelOultram/mumbledj) MumbleDJ
======== ========
**A Mumble bot that plays music fetched from YouTube videos.** **A Mumble bot that plays music fetched from YouTube videos and Soundcloud tracks.**
* [Usage](#usage) * [Usage](#usage)
* [Features](#features) * [Features](#features)
@ -31,7 +31,8 @@ All commandline parameters are optional. Below are descriptions of all the avail
* `-accesstokens`: List of access tokens for the bot separated by spaces. Defaults to no access tokens. * `-accesstokens`: List of access tokens for the bot separated by spaces. Defaults to no access tokens.
## FEATURES ## FEATURES
* Plays audio from both YouTube videos and YouTube playlists! * Plays audio from YouTube and Soundcloud!
* Supports playlists and individual videos/tracks.
* Displays thumbnail, title, duration, submitter, and playlist title (if exists) when a new song is played. * Displays thumbnail, title, duration, submitter, and playlist title (if exists) when a new song is played.
* Incredible customization options. Nearly everything is able to be tweaked in `~/.mumbledj/mumbledj.gcfg`. * Incredible customization options. Nearly everything is able to be tweaked in `~/.mumbledj/mumbledj.gcfg`.
* A large array of [commands](#commands) that perform a wide variety of functions. * A large array of [commands](#commands) that perform a wide variety of functions.
@ -43,7 +44,7 @@ These are all of the chat commands currently supported by MumbleDJ. All command
Command | Description | Arguments | Admin | Example Command | Description | Arguments | Admin | Example
--------|-------------|-----------|-------|-------- --------|-------------|-----------|-------|--------
**add** | Adds a YouTube video's audio to the song queue. If no songs are currently in the queue, the audio will begin playing immediately. YouTube playlists may also be added using this command. Please note, however, that if a YouTube playlist contains over 25 videos only the first 25 videos will be placed in the song queue. | youtube_video_url OR youtube_playlist_url | No | `!add https://www.youtube.com/watch?v=5xfEr2Oxdys` **add** | Adds audio from a url to the song queue. If no songs are currently in the queue, the audio will begin playing immediately. Playlists may also be added using this command. Please note, however, that if a YouTube playlist contains over 25 videos only the first 25 videos will be placed in the song queue. | youtube_video_url OR youtube_playlist_url OR soundcloud_track_url OR soundcloud_playlist_url | No | `!add https://www.youtube.com/watch?v=5xfEr2Oxdys`
**skip**| Submits a vote to skip the current song. Once the skip ratio target (specified in `mumbledj.gcfg`) is met, the song will be skipped and the next will start playing. Each user may only submit one skip per song. | None | No | `!skip` **skip**| Submits a vote to skip the current song. Once the skip ratio target (specified in `mumbledj.gcfg`) is met, the song will be skipped and the next will start playing. Each user may only submit one skip per song. | None | No | `!skip`
**skipplaylist** | Submits a vote to skip the current playlist. Once the skip ratio target (specified in mumbledj.gcfg) is met, the playlist will be skipped and the next song/playlist will start playing. Each user may only submit one skip per playlist. | None | No | `!skipplaylist` **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` **forceskip** | An admin command that forces a song skip. | None | Yes | `!forceskip`
@ -85,7 +86,7 @@ Effective April 20th, 2015, all requests to YouTube's API must use v3 of their A
**7)** Open up `~/.bashrc` with your favorite text editor (or `~/.zshrc` if you use `zsh`). Add the following line to the bottom: `export YOUTUBE_API_KEY="<your_key_here>"`. Replace \<your_key_here\> with your API key. **7)** Open up `~/.bashrc` with your favorite text editor (or `~/.zshrc` if you use `zsh`). Add the following line to the bottom: `export YOUTUBE_API_KEY="<your_key_here>"`. Replace \<your_key_here\> with your API key.
**8)** Close your current terminal window and open another one up. You should be able to use MumbleDJ now! **8)** Close your current terminal window and open another one up. You should be able to use Youtube on MumbleDJ now!
###SOUNDCLOUD API KEYS ###SOUNDCLOUD API KEYS
A soundcloud API key is required for soundcloud integration. If no soundcloud api key is found, then the service will be disabled (youtube links will still work however). 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).

View file

@ -1,27 +0,0 @@
machine:
environment:
PATH: $HOME/bin/:$PATH
LD_RUN_PATH: $LD_RUN_PATH:$HOME/opus/lib
LD_LIBRARY_PATH: $LD_LIBRARY_PATH:$HOME/opus/lib
PKG_CONFIG_PATH: $PKG_CONFIG_PATH:$HOME/opus/lib/pkgconfig
dependencies:
pre:
- bash install-dependencies.sh
override:
- make
- make install
cache_directories:
- "~/opus"
- "~/bin"
- "~/mumbledj/.vendor"
- "/home/ubuntu/.go_workspace/bin"
- "/home/ubuntu/.go_workspace/pkg"
- "/home/ubuntu/.go_workspace/src/github.com/nitrous-io"
test:
override:
- mumbledj -server=$MUMBLE_IP -port=$MUMBLE_PORT -username=circleci -password=$MUMBLE_PASSWORD -test=true:
timeout: 180

View file

@ -140,7 +140,7 @@ AdminsEnabled = true
# SYNTAX: In order to specify multiple admins, repeat the Admins="username" # SYNTAX: In order to specify multiple admins, repeat the Admins="username"
# line of code. Each line has one username, and an unlimited amount of usernames may # line of code. Each line has one username, and an unlimited amount of usernames may
# be entered in this matter. # be entered in this matter.
Admins = "BottleOToast" Admins = "Matt"
# Make add an admin command? # Make add an admin command?
# DEFAULT VALUE: false # DEFAULT VALUE: false

View file

@ -1,37 +0,0 @@
#!/bin/sh
set -e
# removing old ffmpeg
sudo rm -rf /usr/bin/ffmpeg
sudo rm -rf /usr/bin/X11/ffmpeg
sudo rm -rf /usr/share/man/man1/ffmpeg.1.gz
# check to see if ffmpeg is installed
if [ ! -f "$HOME/bin/ffmpeg" ]; then
echo 'Installing ffmpeg'
wget http://johnvansickle.com/ffmpeg/releases/ffmpeg-release-64bit-static.tar.xz -O /tmp/ffmpeg.tar.xz
tar -xvf /tmp/ffmpeg.tar.xz --strip 1 --no-anchored ffmpeg ffprobe
chmod a+rx ffmpeg ffprobe
mv ff* ~/bin
else
echo 'Using cached version of ffmpeg.';
fi
# check to see if youtube-dl is installed
if [ ! -f "$HOME/bin/youtube-dl" ]; then
echo 'Installing youtube-dl'
curl https://yt-dl.org/latest/youtube-dl -o ~/bin/youtube-dl
chmod a+rx ~/bin/youtube-dl
else
echo 'Using cached version of youtube-dl.';
fi
# check to see if opus is installed
if [ ! -d "$HOME/opus/lib" ]; then
echo 'Installing opus'
wget http://downloads.xiph.org/releases/opus/opus-1.0.3.tar.gz
tar xzvf opus-1.0.3.tar.gz
cd opus-1.0.3 && ./configure --prefix=$HOME/opus && make && make install
else
echo 'Using cached version of opus.';
fi

View file

@ -153,7 +153,7 @@ func CheckAPIKeys() {
// Checks to see if any service was disabled // Checks to see if any service was disabled
if anyDisabled { if anyDisabled {
fmt.Printf("Please see the following link for info on how to enable services: https://github.com/matthieugrieger/mumbledj\n") 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 // Exits application if no services are enabled
@ -194,7 +194,7 @@ func main() {
} }
var address, port, username, password, channel, pemCert, pemKey, accesstokens string var address, port, username, password, channel, pemCert, pemKey, accesstokens string
var insecure, testcode bool var insecure bool
flag.StringVar(&address, "server", "localhost", "address for Mumble server") flag.StringVar(&address, "server", "localhost", "address for Mumble server")
flag.StringVar(&port, "port", "64738", "port for Mumble server") flag.StringVar(&port, "port", "64738", "port for Mumble server")
@ -205,7 +205,6 @@ func main() {
flag.StringVar(&pemKey, "key", "", "path to user PEM key for MumbleDJ") flag.StringVar(&pemKey, "key", "", "path to user PEM key for MumbleDJ")
flag.StringVar(&accesstokens, "accesstokens", "", "list of access tokens for channel auth") flag.StringVar(&accesstokens, "accesstokens", "", "list of access tokens for channel auth")
flag.BoolVar(&insecure, "insecure", false, "skip certificate checking") flag.BoolVar(&insecure, "insecure", false, "skip certificate checking")
flag.BoolVar(&testcode, "test", false, "[debug] tests the features of mumbledj")
flag.Parse() flag.Parse()
dj.config = gumble.Config{ dj.config = gumble.Config{
@ -246,8 +245,5 @@ func main() {
os.Exit(1) os.Exit(1)
} }
if testcode {
Test(password, address, port, strings.Split(accesstokens, " "))
}
<-dj.keepAlive <-dj.keepAlive
} }

View file

@ -54,6 +54,8 @@ type Playlist interface {
var services []Service 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 { func FindServiceAndAdd(user *gumble.User, url string) error {
var urlService Service var urlService Service

View file

@ -1,3 +1,10 @@
/*
* MumbleDJ
* By Matthieu Grieger
* service_soundcloud.go
* Copyright (c) 2014, 2015 Matthieu Grieger (MIT License)
*/
package main package main
import ( import (
@ -5,6 +12,8 @@ import (
"fmt" "fmt"
"os" "os"
"strconv" "strconv"
"strings"
"time"
"github.com/jmoiron/jsonq" "github.com/jmoiron/jsonq"
"github.com/layeh/gumble/gumble" "github.com/layeh/gumble/gumble"
@ -30,7 +39,8 @@ func (sc SoundCloud) URLRegex(url string) bool {
func (sc SoundCloud) NewRequest(user *gumble.User, url string) (string, error) { func (sc SoundCloud) NewRequest(user *gumble.User, url string) (string, error) {
var apiResponse *jsonq.JsonQuery var apiResponse *jsonq.JsonQuery
var err error var err error
url = fmt.Sprintf("http://api.soundcloud.com/resolve?url=%s&client_id=%s", url, os.Getenv("SOUNDCLOUD_API_KEY")) timesplit := strings.Split(url, "#t=")
url = fmt.Sprintf("http://api.soundcloud.com/resolve?url=%s&client_id=%s", timesplit[0], os.Getenv("SOUNDCLOUD_API_KEY"))
if apiResponse, err = PerformGetRequest(url); err != nil { if apiResponse, err = PerformGetRequest(url); err != nil {
return "", errors.New(INVALID_API_KEY) return "", errors.New(INVALID_API_KEY)
} }
@ -49,27 +59,32 @@ func (sc SoundCloud) NewRequest(user *gumble.User, url string) (string, error) {
// Add all tracks // Add all tracks
for _, t := range tracks { for _, t := range tracks {
sc.NewSong(user, jsonq.NewQuery(t), playlist) sc.NewSong(user, jsonq.NewQuery(t), 0, playlist)
} }
if err == nil { return playlist.Title(), nil
return playlist.Title(), nil
} else {
return "", err
}
} else {
return "", errors.New("NO_PLAYLIST_PERMISSION")
} }
return "", errors.New(NO_PLAYLIST_PERMISSION_MSG)
} else { } else {
// SONG // SONG
return sc.NewSong(user, apiResponse, nil) 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
}
}
return sc.NewSong(user, apiResponse, offset, nil)
} }
} }
// NewSong creates a track and adds to the queue // NewSong creates a track and adds to the queue
func (sc SoundCloud) NewSong(user *gumble.User, trackData *jsonq.JsonQuery, playlist Playlist) (string, error) { func (sc SoundCloud) NewSong(user *gumble.User, trackData *jsonq.JsonQuery, offset int, playlist Playlist) (string, error) {
title, _ := trackData.String("title") title, _ := trackData.String("title")
id, _ := trackData.Int("id") id, _ := trackData.Int("id")
duration, _ := trackData.Int("duration") durationMS, _ := trackData.Int("duration")
url, _ := trackData.String("permalink_url") url, _ := trackData.String("permalink_url")
thumbnail, err := trackData.String("artwork_url") thumbnail, err := trackData.String("artwork_url")
if err != nil { if err != nil {
@ -78,14 +93,19 @@ func (sc SoundCloud) NewSong(user *gumble.User, trackData *jsonq.JsonQuery, play
thumbnail, _ = jsonq.NewQuery(userObj).String("avatar_url") thumbnail, _ = jsonq.NewQuery(userObj).String("avatar_url")
} }
if dj.conf.General.MaxSongDuration == 0 || (duration/1000) <= dj.conf.General.MaxSongDuration { // Check song is not longer than the MaxSongDuration
if dj.conf.General.MaxSongDuration == 0 || (durationMS/1000) <= dj.conf.General.MaxSongDuration {
timeDuration, _ := time.ParseDuration(strconv.Itoa(durationMS/1000) + "s")
duration := strings.NewReplacer("h", ":", "m", ":", "s", "").Replace(timeDuration.String())
song := &YouTubeSong{ song := &YouTubeSong{
id: strconv.Itoa(id), id: strconv.Itoa(id),
title: title, title: title,
url: url, url: url,
thumbnail: thumbnail, thumbnail: thumbnail,
submitter: user, submitter: user,
duration: strconv.Itoa(duration), duration: duration,
offset: offset,
format: "mp3", format: "mp3",
playlist: playlist, playlist: playlist,
skippers: make([]string, 0), skippers: make([]string, 0),

View file

@ -32,17 +32,9 @@ var youtubeVideoPatterns = []string{
`https?:\/\/www.youtube.com\/v\/([\w-]+)(\?t=\d*m?\d*s?)?`, `https?:\/\/www.youtube.com\/v\/([\w-]+)(\?t=\d*m?\d*s?)?`,
} }
// ------
// TYPES
// ------
// YouTube implements the Service interface // YouTube implements the Service interface
type YouTube struct{} type YouTube struct{}
// ---------------
// YOUTUBE SERVICE
// ---------------
// URLRegex checks to see if service will accept URL // URLRegex checks to see if service will accept URL
func (yt YouTube) URLRegex(url string) bool { func (yt YouTube) URLRegex(url string) bool {
return RegexpFromURL(url, append(youtubeVideoPatterns, []string{youtubePlaylistPattern}...)) != nil return RegexpFromURL(url, append(youtubeVideoPatterns, []string{youtubePlaylistPattern}...)) != nil

85
test.go
View file

@ -1,85 +0,0 @@
package main
import (
"fmt"
"github.com/layeh/gumble/gumble"
"os"
"time"
)
type TestSettings struct {
password string
ip string
port string
accesstokens []string
}
var test TestSettings
func Test(password, ip, port string, accesstokens []string) {
test = TestSettings{
password: password,
ip: ip,
port: port,
accesstokens: accesstokens,
}
test.testYoutubeSong()
}
func (t TestSettings) createClient(uname string) *gumble.Client {
config := gumble.Config{
Username: uname,
Password: t.password,
Address: t.ip + ":" + t.port,
Tokens: t.accesstokens,
}
config.TLSConfig.InsecureSkipVerify = true
client := gumble.NewClient(&config)
return client
}
func (t TestSettings) testYoutubeSong() {
dummyClient := t.createClient("dummy")
if err := dummyClient.Connect(); err != nil {
panic(err)
}
dj.client.Request(gumble.RequestUserList)
time.Sleep(time.Second * 5)
dummyUser := dj.client.Users.Find("dummy")
if dummyUser == nil {
fmt.Printf("User does not exist, printing users\n")
for _, user := range dj.client.Users {
fmt.Printf(user.Name + "\n")
}
fmt.Printf("End of user list\n")
os.Exit(1)
}
// Don't judge, I used the (autogenerated) Top Tracks for United Kingdom playlist
songs := map[string]string{
"http://www.youtube.com/watch?v=QcIy9NiNbmo": "Taylor Swift - Bad Blood ft. Kendrick Lamar",
"https://www.youtube.com/watch?v=vjW8wmF5VWc": "Silentó - Watch Me (Whip/Nae Nae) (Official)",
"http://youtu.be/nsDwItoNlLc": "Tinie Tempah ft. Jess Glynne - Not Letting Go (Official Video)",
"https://youtu.be/hXTAn4ELEwM": "Years & Years - Shine",
"http://youtube.com/watch?v=RgKAFK5djSk": "Wiz Khalifa - See You Again ft. Charlie Puth [Official Video] Furious 7 Soundtrack",
"https://youtube.com/watch?v=qWWSM3wCiKY": "Calvin Harris & Disciples - How Deep Is Your Love (Audio)",
"http://www.youtube.com/v/yzTuBuRdAyA": "The Weeknd - The Hills",
"https://www.youtube.com/v/cNw8A5pwbVI": "Pia Mia - Do It Again ft. Chris Brown, Tyga",
}
for url, title := range songs {
err := add(dummyUser, url)
if err != nil {
fmt.Printf("For: %s; Expected: %s; Got: %s\n", url, title, err.Error())
} else if dj.queue.CurrentSong().Title() != title {
fmt.Printf("For: %s; Expected: %s; Got: %s\n", url, title, dj.queue.CurrentSong().Title())
}
time.Sleep(time.Second * 10)
skip(dummyUser, false, false)
}
os.Exit(0)
dummyClient.Disconnect()
}

View file

@ -1,3 +1,10 @@
/*
* MumbleDJ
* By Matthieu Grieger
* youtube_dl.go
* Copyright (c) 2014, 2015 Matthieu Grieger (MIT License)
*/
package main package main
import ( import (
@ -42,7 +49,7 @@ func (dl *YouTubeSong) Download() error {
// Checks to see if song is already downloaded // Checks to see if song is already downloaded
if _, err := os.Stat(fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, dl.Filename())); os.IsNotExist(err) { if _, err := os.Stat(fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, dl.Filename())); os.IsNotExist(err) {
cmd := exec.Command("youtube-dl", "--no-mtime", "--output", fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, dl.Filename()), "--format", dl.format, "--prefer-ffmpeg", dl.url) cmd := exec.Command("youtube-dl", "--verbose", "--no-mtime", "--output", fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, dl.Filename()), "--format", dl.format, "--prefer-ffmpeg", dl.url)
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err == nil { if err == nil {
if dj.conf.Cache.Enabled { if dj.conf.Cache.Enabled {
@ -152,7 +159,7 @@ func (dl *YouTubeSong) ID() string {
// Filename returns the filename of the Song. // Filename returns the filename of the Song.
func (dl *YouTubeSong) Filename() string { func (dl *YouTubeSong) Filename() string {
return dl.id + dl.format return dl.id + "." + dl.format
} }
// Duration returns the duration of the Song. // Duration returns the duration of the Song.