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 CleanupInterval time.Duration
23 MaxSizeBytes int64
24}
25
26type VideoBuffer struct {
27 Buffer *bytes.Buffer
28 Length int64
29}
30
31type Client struct {
32 http *http.Client
33 timeouts *volatile.Volatile[string, error]
34 buffers *volatile.Volatile[string, VideoBuffer]
35 Instance string
36 Options ClientOptions
37}
38
39type Video struct {
40 VideoId string `json:"videoId"`
41 Title string `json:"title"`
42 Description string `json:"description"`
43 Uploader string `json:"author"`
44 VideoThumbnails []VideoThumbnail `json:"videoThumbnails"`
45 Duration int `json:"lengthSeconds"`
46 Formats []Format `json:"formatStreams"`
47 Expire time.Time
48 Url string
49 Thumbnail string
50}
51
52func filter[T any](ss []T, test func(T) bool) (ret []T) {
53 for _, s := range ss {
54 if test(s) {
55 ret = append(ret, s)
56 }
57 }
58 return
59}
60
61func NewVideoBuffer(b *bytes.Buffer, l int64) *VideoBuffer {
62 d := new(bytes.Buffer)
63 d.Write(b.Bytes())
64
65 return &VideoBuffer{
66 Buffer: d,
67 Length: l,
68 }
69}
70
71func (vb *VideoBuffer) Clone() *VideoBuffer {
72 return NewVideoBuffer(vb.Buffer, vb.Length)
73}
74
75func (vb *VideoBuffer) ValidateLength() bool {
76 return vb.Length > 0 && vb.Length == int64(vb.Buffer.Len())
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 case http.StatusNotFound:
99 logger.Debug("Video does not exist or can't be retrieved.")
100 return nil, err
101 default:
102 err = c.NewInstance()
103 if err != nil {
104 logger.Error("Could not get a new instance: ", err)
105 time.Sleep(10 * time.Second)
106 }
107 return c.GetVideo(videoId, true)
108 }
109
110 err = CacheVideoDB(*video)
111 if err != nil {
112 logger.Warn("Could not cache video id: ", videoId)
113 logger.Warn(err)
114 }
115 return video, nil
116}
117
118func NewClient(httpClient *http.Client, options ClientOptions) *Client {
119 InitDB()
120 timeouts := volatile.NewVolatile[string, error](options.TimeoutDuration, options.CleanupInterval)
121 buffers := volatile.NewVolatile[string, VideoBuffer](options.CacheDuration, options.CleanupInterval)
122 client := &Client{
123 http: httpClient,
124 timeouts: timeouts,
125 buffers: buffers,
126 Options: options,
127 }
128 err := client.NewInstance()
129 if err != nil {
130 logger.Fatal(err)
131 }
132 return client
133}