invidious/invidious.go (view raw)
1package invidious
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "io"
8 "log"
9 "net/http"
10 "net/url"
11 "os"
12 "regexp"
13 "strconv"
14 "time"
15)
16
17const maxSizeMB = 50
18const instancesEndpoint = "https://api.invidious.io/instances.json?sort_by=api,type"
19const videosEndpoint = "https://%s/api/v1/videos/%s?fields=videoId,title,description,author,lengthSeconds,size,formatStreams"
20
21var expireRegex = regexp.MustCompile(`(?i)expire=(\d+)`)
22
23type Client struct {
24 http *http.Client
25 Instance string
26}
27
28type Format struct {
29 VideoId string
30 Name string `json:"qualityLabel"`
31 Height int
32 Width int
33 Url string `json:"url"`
34 Container string `json:"container"`
35 Size string `json:"size"`
36}
37
38type Video struct {
39 VideoId string `json:"videoId"`
40 Title string `json:"title"`
41 Description string `json:"description"`
42 Uploader string `json:"author"`
43 Duration int `json:"lengthSeconds"`
44 Formats []Format `json:"formatStreams"`
45 Timestamp time.Time
46 Expire time.Time
47 FormatIndex int
48}
49
50func filter[T any](ss []T, test func(T) bool) (ret []T) {
51 for _, s := range ss {
52 if test(s) {
53 ret = append(ret, s)
54 }
55 }
56 return
57}
58
59func parseOrZero(number string) int {
60 res, err := strconv.Atoi(number)
61 if err != nil {
62 return 0
63 }
64 return res
65}
66
67func (c *Client) fetchVideo(videoId string) (*Video, error) {
68 if c.Instance == "" {
69 err := c.NewInstance()
70 if err != nil {
71 log.Fatal(err, "Could not get a new instance.")
72 os.Exit(1)
73 }
74 }
75 endpoint := fmt.Sprintf(videosEndpoint, c.Instance, url.QueryEscape(videoId))
76 resp, err := c.http.Get(endpoint)
77 if err != nil {
78 return nil, err
79 }
80 defer resp.Body.Close()
81
82 body, err := io.ReadAll(resp.Body)
83 if err != nil {
84 return nil, err
85 }
86
87 if resp.StatusCode != http.StatusOK {
88 return nil, fmt.Errorf(string(body))
89 }
90
91 res := &Video{}
92 err = json.Unmarshal(body, res)
93 if err != nil {
94 return nil, err
95 }
96
97 mp4Test := func(f Format) bool { return f.Container == "mp4" }
98 res.Formats = filter(res.Formats, mp4Test)
99
100 expireString := expireRegex.FindStringSubmatch(res.Formats[0].Url)
101 expireTimestamp, err := strconv.ParseInt(expireString[1], 10, 64)
102 if err != nil {
103 fmt.Println("Error:", err)
104 return nil, err
105 }
106 res.Expire = time.Unix(expireTimestamp, 0)
107
108 return res, err
109}
110
111func (c *Client) GetVideo(videoId string) (*Video, error) {
112 log.Println("Video", videoId, "was requested.")
113
114 video, err := GetVideoDB(videoId)
115 if err == nil {
116 log.Println("Found a valid cache entry.")
117 return video, nil
118 }
119
120 video, err = c.fetchVideo(videoId)
121
122 if err != nil {
123 if err.Error() == "{}" {
124 return nil, err
125 }
126 log.Println(err)
127 err = c.NewInstance()
128 if err != nil {
129 log.Fatal("Could not get a new instance: ", err)
130 time.Sleep(10 * time.Second)
131 }
132 return c.GetVideo(videoId)
133 }
134 log.Println("Retrieved by API.")
135
136 CacheVideoDB(*video)
137 return video, nil
138}
139
140func (c *Client) NewInstance() error {
141 resp, err := c.http.Get(instancesEndpoint)
142 if err != nil {
143 return err
144 }
145 defer resp.Body.Close()
146
147 body, err := io.ReadAll(resp.Body)
148 if err != nil {
149 return err
150 }
151
152 if resp.StatusCode != http.StatusOK {
153 return fmt.Errorf(string(body))
154 }
155
156 var jsonArray [][]interface{}
157 err = json.Unmarshal(body, &jsonArray)
158 if err != nil {
159 return err
160 }
161
162 c.Instance = jsonArray[0][0].(string)
163 log.Println("Using new instance:", c.Instance)
164 return nil
165}
166
167func (c *Client) ProxyVideo(w http.ResponseWriter, videoId string, formatIndex int) error {
168 video, err := GetVideoDB(videoId)
169 if err != nil {
170 http.Error(w, "Bad Request", http.StatusBadRequest)
171 return err
172 }
173
174 fmtAmount := len(video.Formats)
175 idx := formatIndex % fmtAmount
176 url := video.Formats[fmtAmount-1-idx].Url
177 req, err := http.NewRequest(http.MethodGet, url, nil)
178 if err != nil {
179 log.Fatal(err)
180 new_video, err := c.fetchVideo(videoId)
181 if err != nil {
182 log.Fatal("Url for", videoId, "expired:", err)
183 return err
184 }
185 return c.ProxyVideo(w, new_video.VideoId, formatIndex)
186 }
187
188 req.Header.Add("Range", fmt.Sprintf("bytes=0-%d000000", maxSizeMB))
189 resp, err := c.http.Do(req)
190 if err != nil {
191 log.Fatal(err)
192 http.Error(w, err.Error(), http.StatusInternalServerError)
193 return err
194 }
195 defer resp.Body.Close()
196
197 w.Header().Set("content-type", "video/mp4")
198 w.Header().Set("Status", "200")
199
200 temp := bytes.NewBuffer(nil)
201 _, err = io.Copy(temp, resp.Body)
202 if err == nil { // done
203 _, err = io.Copy(w, temp)
204 return err
205 }
206
207 newIndex := formatIndex + 1
208 if newIndex < fmtAmount {
209 return c.ProxyVideo(w, videoId, newIndex)
210 }
211 _, err = io.Copy(w, temp)
212 return err
213}
214
215func NewClient(httpClient *http.Client) *Client {
216 InitDB()
217 return &Client{httpClient, ""}
218}