invidious/invidious.go (view raw)
1package invidious
2
3import (
4 "net/http"
5 "regexp"
6 "strconv"
7 "time"
8
9 "github.com/sirupsen/logrus"
10)
11
12const timeoutDuration = 10 * time.Minute
13const maxSizeBytes = 20000000 // 20 MB
14const instancesEndpoint = "https://api.invidious.io/instances.json?sort_by=api,type"
15const videosEndpoint = "https://%s/api/v1/videos/%s?fields=videoId,title,description,author,lengthSeconds,size,formatStreams"
16
17var expireRegex = regexp.MustCompile(`(?i)expire=(\d+)`)
18var logger = logrus.New()
19
20type Timeout struct {
21 Instance string
22 Timestamp time.Time
23}
24
25type Client struct {
26 http *http.Client
27 timeouts []Timeout
28 Instance string
29}
30
31type Format struct {
32 Name string `json:"qualityLabel"`
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 Url string
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) GetVideo(videoId string, fromCache bool) (*Video, error) {
68 logger.Info("Video https://youtu.be/", videoId, " was requested.")
69
70 var video *Video
71 var err error
72
73 if fromCache {
74 video, err = GetVideoDB(videoId)
75 if err == nil {
76 logger.Info("Found a valid cache entry.")
77 return video, nil
78 }
79 }
80
81 video, httpErr := c.fetchVideo(videoId)
82
83 switch httpErr {
84 case http.StatusOK:
85 logger.Info("Retrieved by API.")
86 break
87 case http.StatusNotFound:
88 logger.Debug("Video does not exist or can't be retrieved.")
89 return nil, err
90 default:
91 fallthrough
92 case http.StatusInternalServerError:
93 err = c.NewInstance()
94 if err != nil {
95 logger.Error("Could not get a new instance: ", err)
96 time.Sleep(10 * time.Second)
97 }
98 return c.GetVideo(videoId, true)
99 }
100
101 err = CacheVideoDB(*video)
102 if err != nil {
103 logger.Warn("Could not cache video id: ", videoId)
104 logger.Warn(err)
105 }
106 return video, nil
107}
108
109func NewClient(httpClient *http.Client) *Client {
110 InitDB()
111 client := &Client{
112 http: httpClient,
113 timeouts: []Timeout{},
114 }
115 err := client.NewInstance()
116 if err != nil {
117 logger.Fatal(err)
118 }
119 return client
120}