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