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