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 "strconv"
12 "strings"
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 timeToLive, _ = time.ParseDuration("6h")
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 FormatIndex int
46}
47
48func filter[T any](ss []T, test func(T) bool) (ret []T) {
49 for _, s := range ss {
50 if test(s) {
51 ret = append(ret, s)
52 }
53 }
54 return
55}
56
57func parseOrZero(number string) int {
58 res, err := strconv.Atoi(number)
59 if err != nil {
60 return 0
61 }
62 return res
63}
64
65func (c *Client) fetchVideo(videoId string) (*Video, error) {
66 if c.Instance == "" {
67 err := c.NewInstance()
68 if err != nil {
69 log.Fatal(err, "Could not get a new instance.")
70 os.Exit(1)
71 }
72 }
73 endpoint := fmt.Sprintf(videosEndpoint, c.Instance, url.QueryEscape(videoId))
74 resp, err := c.http.Get(endpoint)
75 if err != nil {
76 return nil, err
77 }
78 defer resp.Body.Close()
79
80 body, err := io.ReadAll(resp.Body)
81 if err != nil {
82 return nil, err
83 }
84
85 if resp.StatusCode != http.StatusOK {
86 return nil, fmt.Errorf(string(body))
87 }
88
89 res := &Video{}
90 err = json.Unmarshal(body, res)
91 if err != nil {
92 return nil, err
93 }
94
95 mp4Test := func(f Format) bool { return f.Container == "mp4" }
96 res.Formats = filter(res.Formats, mp4Test)
97
98 for _, f := range res.Formats {
99 s := strings.Split(f.Size, "x")
100 f.Width = parseOrZero(s[0])
101 f.Height = parseOrZero(s[1])
102 }
103
104 return res, err
105}
106
107func (c *Client) GetVideo(videoId string) (*Video, error) {
108 log.Println("Video", videoId, "was requested.")
109
110 video, err := GetVideoDB(videoId)
111 if err == nil {
112 log.Println("Found a valid cache entry.")
113 return video, nil
114 }
115
116 video, err = c.fetchVideo(videoId)
117 if err != nil {
118 log.Println(err)
119 err = c.NewInstance()
120 if err != nil {
121 log.Fatal("Could not get a new instance: ", err)
122 time.Sleep(10)
123 }
124 return c.GetVideo(videoId)
125 }
126 log.Println("Retrieved by API.")
127
128 CacheVideoDB(*video)
129 return video, nil
130}
131
132func (c *Client) NewInstance() error {
133 resp, err := c.http.Get(instancesEndpoint)
134 if err != nil {
135 return err
136 }
137 defer resp.Body.Close()
138
139 body, err := io.ReadAll(resp.Body)
140 if err != nil {
141 return err
142 }
143
144 if resp.StatusCode != http.StatusOK {
145 return fmt.Errorf(string(body))
146 }
147
148 var jsonArray [][]interface{}
149 err = json.Unmarshal(body, &jsonArray)
150 if err != nil {
151 return err
152 }
153
154 c.Instance = jsonArray[0][0].(string)
155 log.Println("Using new instance:", c.Instance)
156 return nil
157}
158
159func (c *Client) ProxyVideo(w http.ResponseWriter, videoId string, formatIndex int) error {
160 video, err := GetVideoDB(videoId)
161 if err != nil {
162 http.Error(w, "Bad Request", http.StatusBadRequest)
163 return err
164 }
165
166 idx := formatIndex % len(video.Formats)
167 url := video.Formats[len(video.Formats)-1-idx].Url
168 req, err := http.NewRequest(http.MethodGet, url, nil)
169 if err != nil {
170 log.Fatal(err)
171 new_video, err := c.fetchVideo(videoId)
172 if err != nil {
173 log.Fatal("Url for", videoId, "expired:", err)
174 return err
175 }
176 return c.ProxyVideo(w, new_video.VideoId, formatIndex)
177 }
178
179 req.Header.Add("Range", fmt.Sprintf("bytes=0-%d000000", maxSizeMB))
180 resp, err := c.http.Do(req)
181 if err != nil {
182 log.Fatal(err)
183 http.Error(w, err.Error(), http.StatusInternalServerError)
184 return err
185 }
186 defer resp.Body.Close()
187
188 w.Header().Set("content-type", "video/mp4")
189 w.Header().Set("Status", "200")
190
191 i, err := io.Copy(w, resp.Body)
192 fmt.Println(i, err)
193 return err
194}
195
196func NewClient(httpClient *http.Client) *Client {
197 InitDB()
198 return &Client{httpClient, ""}
199}