diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 73ec672..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -README.md merge=ours \ No newline at end of file diff --git a/Goopfile.lock b/Goopfile.lock deleted file mode 100644 index 887e548..0000000 --- a/Goopfile.lock +++ /dev/null @@ -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 diff --git a/Makefile b/Makefile index 97d8a23..af8cc7e 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ 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 - 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 goop install goop go build diff --git a/README.md b/README.md index 46d2adb..bcab76a 100644 --- a/README.md +++ b/README.md @@ -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) * [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. ## 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. * 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. @@ -43,7 +44,7 @@ These are all of the chat commands currently supported by MumbleDJ. All command 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` **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` @@ -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=""`. Replace \ 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 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). diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 0d99b58..0000000 --- a/circle.yml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/config.gcfg b/config.gcfg index c55e52c..db67c05 100644 --- a/config.gcfg +++ b/config.gcfg @@ -140,7 +140,7 @@ AdminsEnabled = true # SYNTAX: In order to specify multiple admins, repeat the Admins="username" # line of code. Each line has one username, and an unlimited amount of usernames may # be entered in this matter. -Admins = "BottleOToast" +Admins = "Matt" # Make add an admin command? # DEFAULT VALUE: false diff --git a/install-dependencies.sh b/install-dependencies.sh deleted file mode 100644 index cb03750..0000000 --- a/install-dependencies.sh +++ /dev/null @@ -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 \ No newline at end of file diff --git a/main.go b/main.go index 3682ac7..86c45c0 100644 --- a/main.go +++ b/main.go @@ -153,7 +153,7 @@ func CheckAPIKeys() { // Checks to see if any service was disabled 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 @@ -194,7 +194,7 @@ func main() { } 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(&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(&accesstokens, "accesstokens", "", "list of access tokens for channel auth") flag.BoolVar(&insecure, "insecure", false, "skip certificate checking") - flag.BoolVar(&testcode, "test", false, "[debug] tests the features of mumbledj") flag.Parse() dj.config = gumble.Config{ @@ -246,8 +245,5 @@ func main() { os.Exit(1) } - if testcode { - Test(password, address, port, strings.Split(accesstokens, " ")) - } <-dj.keepAlive } diff --git a/service.go b/service.go index a8e400c..097c412 100644 --- a/service.go +++ b/service.go @@ -54,6 +54,8 @@ type Playlist interface { 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 diff --git a/service_soundcloud.go b/service_soundcloud.go index f332002..055a946 100644 --- a/service_soundcloud.go +++ b/service_soundcloud.go @@ -1,3 +1,10 @@ +/* + * MumbleDJ + * By Matthieu Grieger + * service_soundcloud.go + * Copyright (c) 2014, 2015 Matthieu Grieger (MIT License) + */ + package main import ( @@ -5,6 +12,8 @@ import ( "fmt" "os" "strconv" + "strings" + "time" "github.com/jmoiron/jsonq" "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) { var apiResponse *jsonq.JsonQuery var err error - url = fmt.Sprintf("http://api.soundcloud.com/resolve?url=%s&client_id=%s", url, os.Getenv("SOUNDCLOUD_API_KEY")) + 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 { return "", errors.New(INVALID_API_KEY) } @@ -49,27 +59,32 @@ func (sc SoundCloud) NewRequest(user *gumble.User, url string) (string, error) { // Add all 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 - } else { - return "", err - } - } else { - return "", errors.New("NO_PLAYLIST_PERMISSION") + return playlist.Title(), nil } + return "", errors.New(NO_PLAYLIST_PERMISSION_MSG) } else { // 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 -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") id, _ := trackData.Int("id") - duration, _ := trackData.Int("duration") + durationMS, _ := trackData.Int("duration") url, _ := trackData.String("permalink_url") thumbnail, err := trackData.String("artwork_url") 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") } - 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{ id: strconv.Itoa(id), title: title, url: url, thumbnail: thumbnail, submitter: user, - duration: strconv.Itoa(duration), + duration: duration, + offset: offset, format: "mp3", playlist: playlist, skippers: make([]string, 0), diff --git a/service_youtube.go b/service_youtube.go index ef2b4c5..8375dc4 100644 --- a/service_youtube.go +++ b/service_youtube.go @@ -32,17 +32,9 @@ var youtubeVideoPatterns = []string{ `https?:\/\/www.youtube.com\/v\/([\w-]+)(\?t=\d*m?\d*s?)?`, } -// ------ -// TYPES -// ------ - // YouTube implements the Service interface type YouTube struct{} -// --------------- -// YOUTUBE SERVICE -// --------------- - // URLRegex checks to see if service will accept URL func (yt YouTube) URLRegex(url string) bool { return RegexpFromURL(url, append(youtubeVideoPatterns, []string{youtubePlaylistPattern}...)) != nil diff --git a/strings.go b/strings.go index be2c4f7..d48e7a7 100644 --- a/strings.go +++ b/strings.go @@ -169,4 +169,4 @@ const CURRENT_SONG_HTML = ` // playlist is playing. const CURRENT_SONG_PLAYLIST_HTML = ` The song currently playing is "%s", added %s from the playlist "%s". -` \ No newline at end of file +` diff --git a/test.go b/test.go deleted file mode 100644 index 4edbe03..0000000 --- a/test.go +++ /dev/null @@ -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() -} diff --git a/youtube_dl.go b/youtube_dl.go index 64afb97..efd9963 100644 --- a/youtube_dl.go +++ b/youtube_dl.go @@ -1,3 +1,10 @@ +/* + * MumbleDJ + * By Matthieu Grieger + * youtube_dl.go + * Copyright (c) 2014, 2015 Matthieu Grieger (MIT License) + */ + package main import ( @@ -42,7 +49,7 @@ func (dl *YouTubeSong) Download() error { // Checks to see if song is already downloaded if _, err := os.Stat(fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, dl.Filename())); os.IsNotExist(err) { - cmd := exec.Command("youtube-dl", "--no-mtime", "--output", fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, dl.Filename()), "--format", dl.format, "--prefer-ffmpeg", dl.url) + 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() if err == nil { if dj.conf.Cache.Enabled { @@ -152,7 +159,7 @@ func (dl *YouTubeSong) ID() string { // Filename returns the filename of the Song. func (dl *YouTubeSong) Filename() string { - return dl.id + dl.format + return dl.id + "." + dl.format } // Duration returns the duration of the Song.