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