invidious/invidious.go (view raw)
1package invidious
2
3import (
4 "fmt"
5 "net/http"
6 "regexp"
7 "strconv"
8 "time"
9
10 "github.com/sirupsen/logrus"
11)
12
13const timeoutDuration = 10 * time.Minute
14const maxSizeBytes = 20000000 // 20 MB
15const instancesEndpoint = "https://api.invidious.io/instances.json?sort_by=api,type"
16const videosEndpoint = "https://%s/api/v1/videos/%s?fields=videoId,title,description,author,lengthSeconds,size,formatStreams"
17
18var expireRegex = regexp.MustCompile(`(?i)expire=(\d+)`)
19var logger = logrus.New()
20
21type Timeout struct {
22 Instance string
23 Timestamp time.Time
24}
25
26type Client struct {
27 http *http.Client
28 timeouts []Timeout
29 Instance string
30}
31
32type Format struct {
33 VideoId string
34 Name string `json:"qualityLabel"`
35 Height int
36 Width int
37 Url string `json:"url"`
38 Container string `json:"container"`
39 Size string `json:"size"`
40}
41
42type Video struct {
43 VideoId string `json:"videoId"`
44 Title string `json:"title"`
45 Description string `json:"description"`
46 Uploader string `json:"author"`
47 Duration int `json:"lengthSeconds"`
48 Formats []Format `json:"formatStreams"`
49 Timestamp time.Time
50 Expire time.Time
51 FormatIndex int
52}
53
54func filter[T any](ss []T, test func(T) bool) (ret []T) {
55 for _, s := range ss {
56 if test(s) {
57 ret = append(ret, s)
58 }
59 }
60 return
61}
62
63func parseOrZero(number string) int {
64 res, err := strconv.Atoi(number)
65 if err != nil {
66 return 0
67 }
68 return res
69}
70
71type HTTPError struct {
72 StatusCode int
73}
74
75func (e HTTPError) Error() string {
76 return fmt.Sprintf("HTTP error: %d", e.StatusCode)
77}
78
79func (c *Client) GetVideo(videoId string, fromCache bool) (*Video, error) {
80 logger.Info("Video https://youtu.be/", videoId, " was requested.")
81
82 var video *Video
83 var err error
84
85 if fromCache {
86 video, err = GetVideoDB(videoId)
87 if err == nil {
88 logger.Info("Found a valid cache entry.")
89 return video, nil
90 }
91 }
92
93 video, httpErr := c.fetchVideo(videoId)
94
95 switch httpErr {
96 case http.StatusOK:
97 logger.Info("Retrieved by API.")
98 break
99 case http.StatusNotFound:
100 logger.Debug("Video does not exist or can't be retrieved.")
101 return nil, err
102 default:
103 fallthrough
104 case http.StatusInternalServerError:
105 err = c.NewInstance()
106 if err != nil {
107 logger.Error("Could not get a new instance: ", err)
108 time.Sleep(10 * time.Second)
109 }
110 return c.GetVideo(videoId, true)
111 }
112
113 err = CacheVideoDB(*video)
114 if err != nil {
115 logger.Warn("Could not cache video id: ", videoId)
116 logger.Warn(err)
117 }
118 return video, nil
119}
120
121func NewClient(httpClient *http.Client) *Client {
122 InitDB()
123 client := &Client{
124 http: httpClient,
125 timeouts: []Timeout{},
126 Instance: "",
127 }
128 err := client.NewInstance()
129 if err != nil {
130 logger.Fatal(err)
131 }
132 return client
133}