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 = 5
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}