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