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")
19
20type Client struct {
21 http *http.Client
22 Instance string
23}
24
25type Format struct {
26 Url string `json:"url"`
27 Container string `json:"container"`
28 Size string `json:"size"`
29}
30
31type Video struct {
32 VideoId string `json:"videoId"`
33 Title string `json:"title"`
34 Description string `json:"description"`
35 Uploader string `json:"author"`
36 Duration int `json:"lengthSeconds"`
37 FormatStreams []Format `json:"formatStreams"`
38 Url string
39 Height int
40 Width int
41 Timestamp time.Time
42}
43
44func filter[T any](ss []T, test func(T) bool) (ret []T) {
45 for _, s := range ss {
46 if test(s) {
47 ret = append(ret, s)
48 }
49 }
50 return
51}
52
53func parseOrZero(number string) int {
54 res, err := strconv.Atoi(number)
55 if err != nil {
56 return 0
57 }
58 return res
59}
60
61func (c *Client) FetchVideo(videoId string) (*Video, error) {
62 if c.Instance == "" {
63 err := c.NewInstance()
64 if err != nil {
65 log.Fatal(err, "Could not get a new instance.")
66 os.Exit(1)
67 }
68 }
69 endpoint := fmt.Sprintf(videosEndpoint, c.Instance, url.QueryEscape(videoId))
70 resp, err := c.http.Get(endpoint)
71 if err != nil {
72 return nil, err
73 }
74 defer resp.Body.Close()
75
76 body, err := io.ReadAll(resp.Body)
77 if err != nil {
78 return nil, err
79 }
80
81 if resp.StatusCode != http.StatusOK {
82 return nil, fmt.Errorf(string(body))
83 }
84
85 res := &Video{}
86 err = json.Unmarshal(body, res)
87 if err != nil {
88 return nil, err
89 }
90
91 mp4Test := func(f Format) bool { return f.Container == "mp4" }
92 mp4Formats := filter(res.FormatStreams, mp4Test)
93 myFormat := mp4Formats[len(mp4Formats)-1]
94 mySize := strings.Split(myFormat.Size, "x")
95
96 res.Url = myFormat.Url
97 res.Width = parseOrZero(mySize[0])
98 res.Height = parseOrZero(mySize[1])
99
100 return res, err
101}
102
103func (c *Client) GetVideo(videoId string) (*Video, error) {
104 log.Println("Video", videoId, "was requested.")
105
106 video, err := GetVideoDB(videoId)
107 if err == nil {
108 now := time.Now()
109 delta := now.Sub(video.Timestamp)
110 if delta < timeToLive {
111 log.Println("Found a valid cache entry from", delta, "ago.")
112 return video, nil
113 }
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 NewClient(httpClient *http.Client) *Client {
160 InitDB()
161 return &Client{httpClient, ""}
162}