commit
6396413c44
8
Goopfile.lock
Normal file
8
Goopfile.lock
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
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
|
78
commands.go
78
commands.go
|
@ -11,7 +11,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -158,41 +157,31 @@ func parseCommand(user *gumble.User, username, command string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add performs !add functionality. Checks input URL for YouTube format, and adds
|
// add performs !add functionality. Checks input URL for service, and adds
|
||||||
// the URL to the queue if the format matches.
|
// the URL to the queue if the format matches.
|
||||||
func add(user *gumble.User, username, url string) {
|
func add(user *gumble.User, username, url string) {
|
||||||
if url == "" {
|
if url == "" {
|
||||||
dj.SendPrivateMessage(user, NO_ARGUMENT_MSG)
|
dj.SendPrivateMessage(user, NO_ARGUMENT_MSG)
|
||||||
} else {
|
} else {
|
||||||
youtubePatterns := []string{
|
var urlService Service
|
||||||
`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?)?`,
|
|
||||||
}
|
|
||||||
matchFound := false
|
|
||||||
shortURL := ""
|
|
||||||
startOffset := ""
|
|
||||||
|
|
||||||
for _, pattern := range youtubePatterns {
|
// Checks all services to see if any can take the URL
|
||||||
if re, err := regexp.Compile(pattern); err == nil {
|
for _, service := range services {
|
||||||
if re.MatchString(url) {
|
if service.URLRegex(url) {
|
||||||
matchFound = true
|
urlService = service
|
||||||
matches := re.FindAllStringSubmatch(url, -1)
|
|
||||||
shortURL = matches[0][1]
|
|
||||||
if len(matches[0]) == 3 {
|
|
||||||
startOffset = matches[0][2]
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if matchFound {
|
if urlService == nil {
|
||||||
if newSong, err := NewYouTubeSong(username, shortURL, startOffset, nil); err == nil {
|
dj.SendPrivateMessage(user, INVALID_URL_MSG)
|
||||||
dj.client.Self.Channel.Send(fmt.Sprintf(SONG_ADDED_HTML, username, newSong.title), false)
|
} else {
|
||||||
if dj.queue.Len() == 1 && !dj.audioStream.IsPlaying() {
|
oldLength := dj.queue.Len()
|
||||||
|
|
||||||
|
if err := urlService.NewRequest(user, url); err == nil {
|
||||||
|
dj.client.Self.Channel.Send(SONG_ADDED_HTML, false)
|
||||||
|
|
||||||
|
// Starts playing the new song if nothing else is playing
|
||||||
|
if oldLength == 0 && dj.queue.Len() != 0 && !dj.audioStream.IsPlaying() {
|
||||||
if err := dj.queue.CurrentSong().Download(); err == nil {
|
if err := dj.queue.CurrentSong().Download(); err == nil {
|
||||||
dj.queue.CurrentSong().Play()
|
dj.queue.CurrentSong().Play()
|
||||||
} else {
|
} else {
|
||||||
|
@ -201,41 +190,8 @@ func add(user *gumble.User, username, url string) {
|
||||||
dj.queue.OnSongFinished()
|
dj.queue.OnSongFinished()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if fmt.Sprint(err) == "Song exceeds the maximum allowed duration." {
|
|
||||||
dj.SendPrivateMessage(user, VIDEO_TOO_LONG_MSG)
|
|
||||||
} else if fmt.Sprint(err) == "Invalid API key supplied." {
|
|
||||||
dj.SendPrivateMessage(user, INVALID_API_KEY)
|
|
||||||
} else {
|
} else {
|
||||||
dj.SendPrivateMessage(user, INVALID_YOUTUBE_ID_MSG)
|
dj.SendPrivateMessage(user, err.Error())
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Check to see if we have a playlist URL instead.
|
|
||||||
youtubePlaylistPattern := `https?:\/\/www\.youtube\.com\/playlist\?list=([\w-]+)`
|
|
||||||
if re, err := regexp.Compile(youtubePlaylistPattern); err == nil {
|
|
||||||
if re.MatchString(url) {
|
|
||||||
if dj.HasPermission(username, dj.conf.Permissions.AdminAddPlaylists) {
|
|
||||||
shortURL = re.FindStringSubmatch(url)[1]
|
|
||||||
oldLength := dj.queue.Len()
|
|
||||||
if newPlaylist, err := NewYouTubePlaylist(username, shortURL); err == nil {
|
|
||||||
dj.client.Self.Channel.Send(fmt.Sprintf(PLAYLIST_ADDED_HTML, username, newPlaylist.title), false)
|
|
||||||
if oldLength == 0 && dj.queue.Len() != 0 && !dj.audioStream.IsPlaying() {
|
|
||||||
if err := dj.queue.CurrentSong().Download(); err == nil {
|
|
||||||
dj.queue.CurrentSong().Play()
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, AUDIO_FAIL_MSG)
|
|
||||||
dj.queue.CurrentSong().Delete()
|
|
||||||
dj.queue.OnSongFinished()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, INVALID_YOUTUBE_ID_MSG)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, NO_PLAYLIST_PERMISSION_MSG)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dj.SendPrivateMessage(user, INVALID_URL_MSG)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
17
main.go
17
main.go
|
@ -15,6 +15,7 @@ import (
|
||||||
"os/user"
|
"os/user"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"github.com/layeh/gopus"
|
"github.com/layeh/gopus"
|
||||||
"github.com/layeh/gumble/gumble"
|
"github.com/layeh/gumble/gumble"
|
||||||
|
@ -35,6 +36,7 @@ type mumbledj struct {
|
||||||
homeDir string
|
homeDir string
|
||||||
playlistSkips map[string][]string
|
playlistSkips map[string][]string
|
||||||
cache *SongCache
|
cache *SongCache
|
||||||
|
verbose bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnConnect event. First moves MumbleDJ into the default channel specified
|
// OnConnect event. First moves MumbleDJ into the default channel specified
|
||||||
|
@ -139,6 +141,17 @@ func PerformStartupChecks() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Verbose(msg string) {
|
||||||
|
if dj.verbose {
|
||||||
|
fmt.Printf(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
// dj variable declaration. This is done outside of main() to allow global use.
|
||||||
var dj = mumbledj{
|
var dj = mumbledj{
|
||||||
keepAlive: make(chan bool),
|
keepAlive: make(chan bool),
|
||||||
|
@ -164,7 +177,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 bool
|
var insecure, verbose 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")
|
||||||
|
@ -175,6 +188,7 @@ func main() {
|
||||||
flag.StringVar(&pemKey, "key", "", "path to user PEM key for MumbleDJ")
|
flag.StringVar(&pemKey, "key", "", "path to user PEM key for MumbleDJ")
|
||||||
flag.StringVar(&accesstokens, "accesstokens", "", "list of access tokens for channel auth")
|
flag.StringVar(&accesstokens, "accesstokens", "", "list of access tokens for channel auth")
|
||||||
flag.BoolVar(&insecure, "insecure", false, "skip certificate checking")
|
flag.BoolVar(&insecure, "insecure", false, "skip certificate checking")
|
||||||
|
flag.BoolVar(&verbose, "verbose", false, "prints out debug messages to the console")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
dj.config = gumble.Config{
|
dj.config = gumble.Config{
|
||||||
|
@ -201,6 +215,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
dj.defaultChannel = strings.Split(channel, "/")
|
dj.defaultChannel = strings.Split(channel, "/")
|
||||||
|
dj.verbose = verbose
|
||||||
|
|
||||||
dj.client.Attach(gumbleutil.Listener{
|
dj.client.Attach(gumbleutil.Listener{
|
||||||
Connect: dj.OnConnect,
|
Connect: dj.OnConnect,
|
||||||
|
|
13
service.go
13
service.go
|
@ -7,6 +7,17 @@
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service interface. Each service should implement these functions
|
||||||
|
type Service interface {
|
||||||
|
ServiceName() string
|
||||||
|
URLRegex(string) bool // Can service deal with URL
|
||||||
|
NewRequest(*gumble.User, string) error // Create song/playlist and add to the queue
|
||||||
|
}
|
||||||
|
|
||||||
// Song interface. Each service will implement these
|
// Song interface. Each service will implement these
|
||||||
// functions in their Song types.
|
// functions in their Song types.
|
||||||
type Song interface {
|
type Song interface {
|
||||||
|
@ -37,3 +48,5 @@ type Playlist interface {
|
||||||
ID() string
|
ID() string
|
||||||
Title() string
|
Title() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var services = []Service{YouTube{}}
|
||||||
|
|
|
@ -21,9 +21,75 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jmoiron/jsonq"
|
"github.com/jmoiron/jsonq"
|
||||||
|
"github.com/layeh/gumble/gumble"
|
||||||
"github.com/layeh/gumble/gumble_ffmpeg"
|
"github.com/layeh/gumble/gumble_ffmpeg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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 SERVICE
|
||||||
|
// ---------------
|
||||||
|
|
||||||
|
type YouTube struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name of the service
|
||||||
|
func (y YouTube) ServiceName() string {
|
||||||
|
return "Youtube"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks to see if service will accept URL
|
||||||
|
func (y YouTube) URLRegex(url string) bool {
|
||||||
|
return RegexpFromURL(url, append(youtubeVideoPatterns, []string{youtubePlaylistPattern}...)) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates the requested song/playlist and adds to the queue
|
||||||
|
func (y YouTube) NewRequest(user *gumble.User, url string) error {
|
||||||
|
var shortURL, startOffset = "", ""
|
||||||
|
if re, err := regexp.Compile(youtubePlaylistPattern); err == nil {
|
||||||
|
if re.MatchString(url) {
|
||||||
|
if dj.HasPermission(user.Name, dj.conf.Permissions.AdminAddPlaylists) {
|
||||||
|
shortURL = re.FindStringSubmatch(url)[1]
|
||||||
|
_, err := NewYouTubePlaylist(user.Name, shortURL)
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
return errors.New("NO_PLAYLIST_PERMISSION")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
re = RegexpFromURL(url, youtubeVideoPatterns)
|
||||||
|
matches := re.FindAllStringSubmatch(url, -1)
|
||||||
|
shortURL = matches[0][1]
|
||||||
|
if len(matches[0]) == 3 {
|
||||||
|
startOffset = matches[0][2]
|
||||||
|
}
|
||||||
|
_, err := NewYouTubeSong(user.Name, shortURL, startOffset, nil)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ------------
|
// ------------
|
||||||
// YOUTUBE SONG
|
// YOUTUBE SONG
|
||||||
// ------------
|
// ------------
|
||||||
|
@ -50,7 +116,7 @@ func NewYouTubeSong(user, id, offset string, playlist *YouTubePlaylist) (*YouTub
|
||||||
url := fmt.Sprintf("https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails&id=%s&key=%s",
|
url := fmt.Sprintf("https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails&id=%s&key=%s",
|
||||||
id, os.Getenv("YOUTUBE_API_KEY"))
|
id, os.Getenv("YOUTUBE_API_KEY"))
|
||||||
if apiResponse, err = PerformGetRequest(url); err != nil {
|
if apiResponse, err = PerformGetRequest(url); err != nil {
|
||||||
return nil, err
|
return nil, errors.New(INVALID_API_KEY)
|
||||||
}
|
}
|
||||||
|
|
||||||
var offsetDays, offsetHours, offsetMinutes, offsetSeconds int64
|
var offsetDays, offsetHours, offsetMinutes, offsetSeconds int64
|
||||||
|
@ -127,26 +193,33 @@ func NewYouTubeSong(user, id, offset string, playlist *YouTubePlaylist) (*YouTub
|
||||||
duration: durationString,
|
duration: durationString,
|
||||||
thumbnail: thumbnail,
|
thumbnail: thumbnail,
|
||||||
skippers: make([]string, 0),
|
skippers: make([]string, 0),
|
||||||
playlist: nil,
|
playlist: playlist,
|
||||||
dontSkip: false,
|
dontSkip: false,
|
||||||
}
|
}
|
||||||
dj.queue.AddSong(song)
|
dj.queue.AddSong(song)
|
||||||
|
Verbose(song.Submitter() + " added track " + song.Title() + "\n")
|
||||||
|
|
||||||
return song, nil
|
return song, nil
|
||||||
}
|
}
|
||||||
return nil, errors.New("Song exceeds the maximum allowed duration.")
|
return nil, errors.New(VIDEO_TOO_LONG_MSG)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download downloads the song via youtube-dl if it does not already exist on disk.
|
// Download downloads the song via youtube-dl if it does not already exist on disk.
|
||||||
// All downloaded songs are stored in ~/.mumbledj/songs and should be automatically cleaned.
|
// All downloaded songs are stored in ~/.mumbledj/songs and should be automatically cleaned.
|
||||||
func (s *YouTubeSong) Download() error {
|
func (s *YouTubeSong) Download() error {
|
||||||
|
|
||||||
|
// Checks to see if song is already downloaded
|
||||||
if _, err := os.Stat(fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, s.Filename())); os.IsNotExist(err) {
|
if _, err := os.Stat(fmt.Sprintf("%s/.mumbledj/songs/%s", dj.homeDir, s.Filename())); os.IsNotExist(err) {
|
||||||
|
Verbose("Downloading " + s.Title() + "\n")
|
||||||
cmd := exec.Command("youtube-dl", "--output", fmt.Sprintf(`~/.mumbledj/songs/%s`, s.Filename()), "--format", "m4a", "--", s.ID())
|
cmd := exec.Command("youtube-dl", "--output", fmt.Sprintf(`~/.mumbledj/songs/%s`, s.Filename()), "--format", "m4a", "--", s.ID())
|
||||||
if err := cmd.Run(); err == nil {
|
if err := cmd.Run(); err == nil {
|
||||||
if dj.conf.Cache.Enabled {
|
if dj.conf.Cache.Enabled {
|
||||||
dj.cache.CheckMaximumDirectorySize()
|
dj.cache.CheckMaximumDirectorySize()
|
||||||
}
|
}
|
||||||
|
Verbose(s.Title() + " downloaded\n")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
Verbose(s.Title() + " failed to download\n")
|
||||||
return errors.New("Song download failed.")
|
return errors.New("Song download failed.")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -163,7 +236,7 @@ func (s *YouTubeSong) Play() {
|
||||||
if err := dj.audioStream.Play(); err != nil {
|
if err := dj.audioStream.Play(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
} else {
|
} else {
|
||||||
if s.Playlist() == nil {
|
if isNil(s.Playlist()) {
|
||||||
message := `
|
message := `
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -199,6 +272,8 @@ func (s *YouTubeSong) Play() {
|
||||||
dj.client.Self.Channel.Send(fmt.Sprintf(message, s.Thumbnail(), s.ID(),
|
dj.client.Self.Channel.Send(fmt.Sprintf(message, s.Thumbnail(), s.ID(),
|
||||||
s.Title(), s.Duration(), s.Submitter(), s.Playlist().Title()), false)
|
s.Title(), s.Duration(), s.Submitter(), s.Playlist().Title()), false)
|
||||||
}
|
}
|
||||||
|
Verbose("Now playing " + s.Title() + "\n")
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
dj.audioStream.Wait()
|
dj.audioStream.Wait()
|
||||||
dj.queue.OnSongFinished()
|
dj.queue.OnSongFinished()
|
||||||
|
@ -212,8 +287,10 @@ func (s *YouTubeSong) Delete() error {
|
||||||
filePath := fmt.Sprintf("%s/.mumbledj/songs/%s.m4a", dj.homeDir, s.ID())
|
filePath := fmt.Sprintf("%s/.mumbledj/songs/%s.m4a", dj.homeDir, s.ID())
|
||||||
if _, err := os.Stat(filePath); err == nil {
|
if _, err := os.Stat(filePath); err == nil {
|
||||||
if err := os.Remove(filePath); err == nil {
|
if err := os.Remove(filePath); err == nil {
|
||||||
|
Verbose("Deleted " + s.Title() + "\n")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
Verbose("Failed to delete " + s.Title() + "\n")
|
||||||
return errors.New("Error occurred while deleting audio file.")
|
return errors.New("Error occurred while deleting audio file.")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -328,81 +405,20 @@ func NewYouTubePlaylist(user, id string) (*YouTubePlaylist, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve items in playlist
|
// Retrieve items in playlist
|
||||||
url = fmt.Sprintf("https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=25&playlistId=%s&key=%s",
|
url = fmt.Sprintf("https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=2&playlistId=%s&key=%s",
|
||||||
id, os.Getenv("YOUTUBE_API_KEY"))
|
id, os.Getenv("YOUTUBE_API_KEY"))
|
||||||
if apiResponse, err = PerformGetRequest(url); err != nil {
|
if apiResponse, err = PerformGetRequest(url); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
numVideos, _ := apiResponse.Int("pageInfo", "totalResults")
|
numVideos, _ := apiResponse.Int("pageInfo", "totalResults")
|
||||||
if numVideos > 25 {
|
if numVideos > 2 {
|
||||||
numVideos = 25
|
numVideos = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < numVideos; i++ {
|
for i := 0; i < numVideos; i++ {
|
||||||
index := strconv.Itoa(i)
|
index := strconv.Itoa(i)
|
||||||
videoTitle, err := apiResponse.String("items", index, "snippet", "title")
|
|
||||||
videoID, _ := apiResponse.String("items", index, "snippet", "resourceId", "videoId")
|
videoID, _ := apiResponse.String("items", index, "snippet", "resourceId", "videoId")
|
||||||
videoThumbnail, _ := apiResponse.String("items", index, "snippet", "thumbnails", "high", "url")
|
NewYouTubeSong(user, videoID, "", playlist)
|
||||||
|
|
||||||
// A completely separate API call just to get the duration of a video in a
|
|
||||||
// playlist? WHY GOOGLE, WHY?!
|
|
||||||
var durationResponse *jsonq.JsonQuery
|
|
||||||
url = fmt.Sprintf("https://www.googleapis.com/youtube/v3/videos?part=contentDetails&id=%s&key=%s",
|
|
||||||
videoID, os.Getenv("YOUTUBE_API_KEY"))
|
|
||||||
if durationResponse, err = PerformGetRequest(url); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
videoDuration, _ := durationResponse.String("items", "0", "contentDetails", "duration")
|
|
||||||
|
|
||||||
var days, hours, minutes, seconds int64
|
|
||||||
timestampExp := regexp.MustCompile(`P(?P<days>\d+D)?T(?P<hours>\d+H)?(?P<minutes>\d+M)?(?P<seconds>\d+S)?`)
|
|
||||||
timestampMatch := timestampExp.FindStringSubmatch(videoDuration)
|
|
||||||
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 := int((days * 86400) + (hours * 3600) + (minutes * 60) + seconds)
|
|
||||||
var durationString string
|
|
||||||
if hours != 0 {
|
|
||||||
if days != 0 {
|
|
||||||
durationString = fmt.Sprintf("%d:%02d:%02d:%02d", days, hours, minutes, seconds)
|
|
||||||
} else {
|
|
||||||
durationString = fmt.Sprintf("%d:%02d:%02d", hours, minutes, seconds)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
durationString = fmt.Sprintf("%d:%02d", minutes, seconds)
|
|
||||||
}
|
|
||||||
|
|
||||||
if dj.conf.General.MaxSongDuration == 0 || totalSeconds <= dj.conf.General.MaxSongDuration {
|
|
||||||
playlistSong := &YouTubeSong{
|
|
||||||
submitter: user,
|
|
||||||
title: videoTitle,
|
|
||||||
id: videoID,
|
|
||||||
filename: videoID + ".m4a",
|
|
||||||
duration: durationString,
|
|
||||||
thumbnail: videoThumbnail,
|
|
||||||
skippers: make([]string, 0),
|
|
||||||
playlist: playlist,
|
|
||||||
dontSkip: false,
|
|
||||||
}
|
|
||||||
dj.queue.AddSong(playlistSong)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return playlist, nil
|
return playlist, nil
|
||||||
}
|
}
|
||||||
|
|
10
songqueue.go
10
songqueue.go
|
@ -42,14 +42,14 @@ func (q *SongQueue) CurrentSong() Song {
|
||||||
|
|
||||||
// NextSong moves to the next Song in SongQueue. NextSong() removes the first Song in the queue.
|
// NextSong moves to the next Song in SongQueue. NextSong() removes the first Song in the queue.
|
||||||
func (q *SongQueue) NextSong() {
|
func (q *SongQueue) NextSong() {
|
||||||
if q.CurrentSong().Playlist() != nil {
|
if s, err := q.PeekNext(); err == nil {
|
||||||
if s, err := q.PeekNext(); err == nil {
|
if !isNil(q.CurrentSong().Playlist()) && !isNil(s.Playlist()) {
|
||||||
if s.Playlist() != nil && (q.CurrentSong().Playlist().ID() != s.Playlist().ID()) {
|
if q.CurrentSong().Playlist().ID() != s.Playlist().ID() {
|
||||||
q.CurrentSong().Playlist().DeleteSkippers()
|
q.CurrentSong().Playlist().DeleteSkippers()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
q.CurrentSong().Playlist().DeleteSkippers()
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
q.CurrentSong().Playlist().DeleteSkippers()
|
||||||
}
|
}
|
||||||
q.queue = q.queue[1:]
|
q.queue = q.queue[1:]
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue