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