galched-bot/modules/youtube/requester.go
alexvanin f0f31a8415 Add twitch point song requests to the bot
This commits adds new feature for galchedbot: video requests
in the twitch chat via highlighted chat messages. This messages
parsed by the bot and added to the video queue, that can be
accessed by the dedicated web server. Video queue requires
authorization based on random token added to the cookies.
2020-01-11 22:38:51 +03:00

195 lines
4.6 KiB
Go

package youtube
import (
"context"
"errors"
"fmt"
"log"
"regexp"
"strconv"
"strings"
"sync"
"time"
"google.golang.org/api/option"
"google.golang.org/api/youtube/v3"
"galched-bot/modules/settings"
)
const (
YoutubeIDLength = 11
youtubeRegexpID = `^.*((youtu.be\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?\s]*).*`
)
var (
urlRegex = regexp.MustCompile(youtubeRegexpID)
durationRegex = regexp.MustCompile(`P(?P<years>\d+Y)?(?P<months>\d+M)?(?P<days>\d+D)?T?(?P<hours>\d+H)?(?P<minutes>\d+M)?(?P<seconds>\d+S)?`)
)
type (
Video struct {
ID string
Title string
From string
Duration string
Upvotes uint64
Downvotes uint64
Views uint64
}
Requester struct {
mu *sync.RWMutex
srv *youtube.Service
requests []Video
}
)
func New(ctx context.Context, s *settings.Settings) (*Requester, error) {
srv, err := youtube.NewService(ctx, option.WithAPIKey(s.YoutubeToken))
if err != nil {
return nil, err
}
return &Requester{
mu: new(sync.RWMutex),
srv: srv,
}, nil
}
func (r *Requester) AddVideo(query, from string) (string, error) {
var (
id string
err error
)
// try parse video id from the query
id, err = videoID(query)
if err != nil {
// if we can't fo that, then search for the query
resp, err := r.srv.Search.List("snippet").Type("video").MaxResults(1).Q(query).Do()
if err != nil || len(resp.Items) == 0 || resp.Items[0].Id == nil {
return "", fmt.Errorf("cannot parse youtube id: %w", err)
}
id = resp.Items[0].Id.VideoId
}
// get video info from api
resp, err := r.srv.Videos.List("snippet,statistics,contentDetails").Id(id).Do()
if err != nil {
return "", fmt.Errorf("cannot send request to youtube api: %w", err)
}
// check if response have all required fields
if len(resp.Items) == 0 {
return "", errors.New("youtube api response does not contain items")
}
if resp.Items[0].Snippet == nil {
return "", errors.New("youtube api response does not contain snippet")
}
if resp.Items[0].Statistics == nil {
return "", errors.New("youtube api response does not contain statistics")
}
if resp.Items[0].ContentDetails == nil {
return "", errors.New("youtube api response does not contain content details")
}
// check length of the video not more than 5 minutes
if parseDuration(resp.Items[0].ContentDetails.Duration) == 0 {
err = errors.New("видео не должно быть трансляцией")
return err.Error(), err
}
// check video is not live
if parseDuration(resp.Items[0].ContentDetails.Duration) > time.Minute*5 {
err = errors.New("видео должно быть короче 5 минут")
return err.Error(), err
}
r.mu.Lock()
defer r.mu.Unlock()
// check if video already in the queue
for i := range r.requests {
if r.requests[i].ID == id {
err = errors.New("видео уже есть в очереди")
return err.Error(), err
}
}
r.requests = append(r.requests, Video{
ID: id,
From: from,
Duration: strings.ToLower(resp.Items[0].ContentDetails.Duration[2:]),
Title: resp.Items[0].Snippet.Title,
Upvotes: resp.Items[0].Statistics.LikeCount,
Views: resp.Items[0].Statistics.ViewCount,
Downvotes: resp.Items[0].Statistics.DislikeCount,
})
log.Printf("yt: added video < %s > from < %s >\n", resp.Items[0].Snippet.Title, from)
return resp.Items[0].Snippet.Title, nil
}
func (r *Requester) List() []Video {
r.mu.RLock()
defer r.mu.RUnlock()
result := make([]Video, len(r.requests))
copy(result, r.requests)
return result
}
func (r *Requester) Remove(id string) {
r.mu.Lock()
defer r.mu.Unlock()
for i := range r.requests {
if r.requests[i].ID == id {
r.requests = append(r.requests[:i], r.requests[i+1:]...)
return
}
}
}
func videoID(url string) (string, error) {
result := urlRegex.FindStringSubmatch(url)
ln := len(result)
if ln == 0 || len(result[ln-1]) != YoutubeIDLength {
return "", fmt.Errorf("id haven't matched in \"%s\"", url)
}
return result[ln-1], nil
}
func parseDuration(str string) time.Duration {
matches := durationRegex.FindStringSubmatch(str)
years := parseInt64(matches[1])
months := parseInt64(matches[2])
days := parseInt64(matches[3])
hours := parseInt64(matches[4])
minutes := parseInt64(matches[5])
seconds := parseInt64(matches[6])
hour := int64(time.Hour)
minute := int64(time.Minute)
second := int64(time.Second)
return time.Duration(years*24*365*hour + months*30*24*hour + days*24*hour + hours*hour + minutes*minute + seconds*second)
}
func parseInt64(value string) int64 {
if len(value) == 0 {
return 0
}
parsed, err := strconv.Atoi(value[:len(value)-1])
if err != nil {
return 0
}
return int64(parsed)
}