all repos — disgord @ 7ec60f80e6cc38ccdab26e661e02a600046a0a97

A simple Discord bot in Go.

src/music/video.go (view raw)

  1package music
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"net/http"
  7	"regexp"
  8	"strconv"
  9	"strings"
 10	"time"
 11
 12	gl "github.com/birabittoh/disgord/src/globals"
 13	"github.com/birabittoh/myks"
 14	"github.com/kkdai/youtube/v2"
 15)
 16
 17const (
 18	defaultCacheDuration = 6 * time.Hour
 19)
 20
 21var (
 22	expireRegex = regexp.MustCompile(`(?i)expire=(\d+)`)
 23	ks          = myks.New[youtube.Video](time.Hour)
 24)
 25
 26func getFormat(video youtube.Video) *youtube.Format {
 27	formats := video.Formats.Type("audio")
 28	for i, format := range formats {
 29		if format.URL != "" {
 30			return &formats[i]
 31		}
 32	}
 33
 34	return nil
 35}
 36
 37func parseExpiration(url string) time.Duration {
 38	expireString := expireRegex.FindStringSubmatch(url)
 39	if len(expireString) < 2 {
 40		return defaultCacheDuration
 41	}
 42
 43	expireTimestamp, err := strconv.ParseInt(expireString[1], 10, 64)
 44	if err != nil {
 45		return defaultCacheDuration
 46	}
 47
 48	return time.Until(time.Unix(expireTimestamp, 0))
 49}
 50
 51func getFromYT(videoID string) (video *youtube.Video, err error) {
 52	url := "https://youtu.be/" + videoID
 53
 54	const maxRetries = 3
 55	const maxBytesToCheck = 1024
 56	var duration time.Duration
 57
 58	for i := 0; i < maxRetries; i++ {
 59		logger.Info("Requesting video", url, "attempt", i+1)
 60		video, err = yt.GetVideo(url)
 61		if err != nil || video == nil {
 62			logger.Error("Error fetching video info:", err)
 63			continue
 64		}
 65
 66		format := getFormat(*video)
 67		if format == nil {
 68			logger.Errorf("no audio formats available for video %s", videoID)
 69			continue
 70		}
 71
 72		duration = parseExpiration(format.URL)
 73
 74		resp, err := http.Get(format.URL)
 75		if err != nil {
 76			logger.Error("Error fetching video URL:", err)
 77			continue
 78		}
 79		defer resp.Body.Close()
 80
 81		if resp.ContentLength <= 0 {
 82			logger.Error("Invalid video link, no content length...")
 83			continue
 84		}
 85
 86		buffer := make([]byte, maxBytesToCheck)
 87		n, err := resp.Body.Read(buffer)
 88		if err != nil {
 89			logger.Error("Error reading video content:", err)
 90			continue
 91		}
 92
 93		if n > 0 {
 94			logger.Info("Valid video link found.")
 95			ks.Set(videoID, *video, duration)
 96			return video, nil
 97		}
 98
 99		logger.Error("Invalid video link, content is empty...")
100		time.Sleep(1 * time.Second)
101	}
102
103	err = fmt.Errorf("failed to fetch valid video after %d attempts", maxRetries)
104	return nil, err
105}
106
107func getFromCache(videoID string) (video *youtube.Video, err error) {
108	video, err = ks.Get(videoID)
109	if err != nil {
110		return
111	}
112
113	if video == nil {
114		err = errors.New("video should not be nil")
115		return
116	}
117
118	return
119}
120
121func getVideo(args []string) (*youtube.Video, error) {
122	videoID, err := youtube.ExtractVideoID(args[0])
123	if err != nil {
124		searchQuery := strings.Join(args, " ")
125		videoID, err = gl.Search(searchQuery)
126		if err != nil || videoID == "" {
127			return nil, err
128		}
129	}
130
131	video, err := getFromCache(videoID)
132	if err != nil {
133		return getFromYT(videoID)
134	}
135
136	return video, nil
137}