2016-06-21 02:16:05 +02:00
|
|
|
/*
|
|
|
|
* MumbleDJ
|
|
|
|
* By Matthieu Grieger
|
|
|
|
* services/youtube.go
|
|
|
|
* Copyright (c) 2016 Matthieu Grieger (MIT License)
|
|
|
|
*/
|
|
|
|
|
|
|
|
package services
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"math"
|
|
|
|
"net/http"
|
|
|
|
"regexp"
|
2016-11-06 05:30:42 +01:00
|
|
|
"strings"
|
|
|
|
"time"
|
2016-06-21 02:16:05 +02:00
|
|
|
|
|
|
|
"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
|
|
|
|
)
|
|
|
|
|
2016-11-06 05:30:42 +01:00
|
|
|
dummyOffset, _ := time.ParseDuration("0s")
|
|
|
|
urlSplit := strings.Split(url, "?t=")
|
|
|
|
|
2016-06-21 02:16:05 +02:00
|
|
|
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"
|
2016-11-06 05:30:42 +01:00
|
|
|
id, err = yt.getID(urlSplit[0])
|
2016-06-21 02:16:05 +02:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
2016-06-27 06:57:15 +02:00
|
|
|
// YouTube playlist searches return a max of 50 results per page
|
|
|
|
maxResults := 50
|
|
|
|
if maxResults > maxItems {
|
|
|
|
maxResults = maxItems
|
|
|
|
}
|
|
|
|
|
2016-06-21 02:16:05 +02:00
|
|
|
pageToken := ""
|
|
|
|
for len(tracks) < maxItems {
|
2016-06-27 06:57:15 +02:00
|
|
|
curResp, curErr := http.Get(fmt.Sprintf(playlistItemsURL, id, maxResults, viper.GetString("api_keys.youtube"), pageToken))
|
2016-06-21 02:16:05 +02:00
|
|
|
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...
|
2016-11-06 05:30:42 +01:00
|
|
|
newTrack, _ := yt.getTrack(videoID, submitter, dummyOffset)
|
2016-06-21 02:16:05 +02:00
|
|
|
newTrack.Playlist = playlist
|
|
|
|
tracks = append(tracks, newTrack)
|
|
|
|
|
|
|
|
if len(tracks) >= maxItems {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2016-06-27 06:57:15 +02:00
|
|
|
|
|
|
|
pageToken, _ = v.GetString("nextPageToken")
|
|
|
|
if pageToken == "" {
|
|
|
|
break
|
|
|
|
}
|
2016-06-21 02:16:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(tracks) == 0 {
|
|
|
|
return nil, errors.New("Invalid playlist. No tracks were added")
|
|
|
|
}
|
|
|
|
return tracks, nil
|
|
|
|
}
|
|
|
|
|
2016-11-06 05:30:42 +01:00
|
|
|
// Submitter added a track!
|
|
|
|
offset := dummyOffset
|
|
|
|
if len(urlSplit) == 2 {
|
|
|
|
offset, _ = time.ParseDuration(urlSplit[1])
|
|
|
|
}
|
|
|
|
|
|
|
|
track, err = yt.getTrack(id, submitter, offset)
|
2016-06-21 02:16:05 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
tracks = append(tracks, track)
|
|
|
|
return tracks, nil
|
|
|
|
}
|
|
|
|
|
2016-11-06 05:30:42 +01:00
|
|
|
func (yt *YouTube) getTrack(id string, submitter *gumble.User, offset time.Duration) (bot.Track, error) {
|
2016-06-21 02:16:05 +02:00
|
|
|
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")
|
2016-11-06 03:22:58 +01:00
|
|
|
if len(items) == 0 {
|
|
|
|
return bot.Track{}, errors.New("This YouTube video is private")
|
|
|
|
}
|
2016-06-21 02:16:05 +02:00
|
|
|
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{
|
2016-11-06 05:30:42 +01:00
|
|
|
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,
|
|
|
|
PlaybackOffset: offset,
|
|
|
|
Playlist: nil,
|
2016-06-21 02:16:05 +02:00
|
|
|
}, nil
|
|
|
|
}
|