src/app/video.go (view raw)
1package app
2
3import (
4 "errors"
5 "fmt"
6 "log"
7 "regexp"
8 "strconv"
9 "strings"
10 "time"
11
12 g "github.com/birabittoh/gopipe/src/globals"
13 "github.com/kkdai/youtube/v2"
14)
15
16const (
17 maxMB = 20
18 maxContentLength = maxMB * 1048576
19 defaultCacheDuration = 6 * time.Hour
20)
21
22var (
23 expireRegex = regexp.MustCompile(`(?i)expire=(\d+)`)
24)
25
26func parseExpiration(url string) time.Duration {
27 expireString := expireRegex.FindStringSubmatch(url)
28 if len(expireString) < 2 {
29 return defaultCacheDuration
30 }
31
32 expireTimestamp, err := strconv.ParseInt(expireString[1], 10, 64)
33 if err != nil {
34 log.Println("parseExpiration ERROR:", err)
35 return defaultCacheDuration
36 }
37
38 return time.Until(time.Unix(expireTimestamp, 0))
39}
40
41func getFormat(video youtube.Video, formatID int) *youtube.Format {
42 if formatID != 0 {
43 f := video.Formats.Select(formatsSelectFn)
44 l := len(f)
45 if l > 0 {
46 return &f[(formatID-1)%l]
47 }
48 }
49
50 f := video.Formats.Select(formatsSelectFnBest)
51 if len(f) > 0 {
52 return &f[0]
53 }
54
55 return nil
56}
57
58func getCaptions(video youtube.Video) map[string]Captions {
59 c := make(map[string]Captions)
60 for _, caption := range video.CaptionTracks {
61 c[caption.LanguageCode] = Captions{
62 VideoID: video.ID,
63 Language: caption.LanguageCode,
64 URL: caption.BaseURL,
65 }
66 }
67
68 return c
69}
70
71func formatsSelectFn(f youtube.Format) bool {
72 return f.AudioChannels > 0 && f.ContentLength < maxContentLength && strings.HasPrefix(f.MimeType, "video/mp4")
73}
74
75func formatsSelectFnBest(f youtube.Format) bool {
76 return f.AudioChannels > 0 && strings.HasPrefix(f.MimeType, "video/mp4")
77}
78
79func formatsSelectFnAudioVideo(f youtube.Format) bool {
80 return f.AudioChannels > 0 && f.QualityLabel != ""
81}
82
83func formatsSelectFnVideo(f youtube.Format) bool {
84 return f.QualityLabel != "" && f.AudioChannels == 0
85}
86
87func formatsSelectFnAudio(f youtube.Format) bool {
88 return f.QualityLabel == ""
89}
90
91func getURL(videoID string) string {
92 return fmt.Sprintf(fmtYouTubeURL, videoID)
93}
94
95func getFromCache(videoID string, formatID int) (video *youtube.Video, format *youtube.Format, err error) {
96 video, err = g.KS.Get(videoID)
97 if err != nil {
98 return
99 }
100
101 if video == nil {
102 err = errors.New("video should not be nil")
103 return
104 }
105
106 format = getFormat(*video, formatID)
107 return
108}
109
110func getFromYT(videoID string, formatID int) (video *youtube.Video, format *youtube.Format, err error) {
111 url := getURL(videoID)
112
113 const maxRetries = 3
114 const maxBytesToCheck = 1024
115 duration := defaultCacheDuration
116
117 for i := 0; i < maxRetries; i++ {
118 log.Println("Requesting video", url, "attempt", i+1)
119 video, err = g.YT.GetVideo(url)
120 if err != nil || video == nil {
121 log.Println("Error fetching video info:", err)
122 continue
123 }
124
125 format = getFormat(*video, formatID)
126 if format != nil {
127 duration = parseExpiration(format.URL)
128 }
129
130 resp, err := g.C.Get(format.URL)
131 if err != nil {
132 log.Println("Error fetching video URL:", err)
133 continue
134 }
135 defer resp.Body.Close()
136
137 if resp.ContentLength <= 0 {
138 log.Println("Invalid video link, no content length...")
139 continue
140 }
141
142 buffer := make([]byte, maxBytesToCheck)
143 n, err := resp.Body.Read(buffer)
144 if err != nil {
145 log.Println("Error reading video content:", err)
146 continue
147 }
148
149 if n > 0 {
150 log.Println("Valid video link found.")
151 g.KS.Set(videoID, *video, duration)
152 return video, format, nil
153 }
154
155 log.Println("Invalid video link, content is empty...")
156 time.Sleep(1 * time.Second)
157 }
158
159 err = fmt.Errorf("failed to fetch valid video after %d attempts", maxRetries)
160 return nil, nil, err
161}
162
163func getVideo(videoID string, formatID int) (video *youtube.Video, format *youtube.Format, err error) {
164 video, format, err = getFromCache(videoID, formatID)
165 if err != nil {
166 video, format, err = getFromYT(videoID, formatID)
167 }
168 return
169}