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 (c *Client) GetVideo(videoId string, fromCache bool) (*Video, error) {
75 logger.Info("Video https://youtu.be/", videoId, " was requested.")
76
77 var video *Video
78 var err error
79
80 if fromCache {
81 video, err = GetVideoDB(videoId)
82 if err == nil {
83 logger.Info("Found a valid cache entry.")
84 return video, nil
85 }
86 }
87
88 video, httpErr := c.fetchVideo(videoId)
89
90 switch httpErr {
91 case http.StatusOK:
92 logger.Info("Retrieved by API.")
93 break
94 case http.StatusNotFound:
95 logger.Debug("Video does not exist or can't be retrieved.")
96 return nil, err
97 default:
98 err = c.NewInstance()
99 if err != nil {
100 logger.Error("Could not get a new instance: ", err)
101 time.Sleep(10 * time.Second)
102 }
103 return c.GetVideo(videoId, true)
104 }
105
106 err = CacheVideoDB(*video)
107 if err != nil {
108 logger.Warn("Could not cache video id: ", videoId)
109 logger.Warn(err)
110 }
111 return video, nil
112}
113
114func NewClient(httpClient *http.Client) *Client {
115 InitDB()
116 timeouts := volatile.NewVolatile[string, error](timeoutDuration)
117 buffers := volatile.NewVolatile[string, VideoBuffer](cacheDuration)
118 client := &Client{
119 http: httpClient,
120 timeouts: timeouts,
121 buffers: buffers,
122 }
123 err := client.NewInstance()
124 if err != nil {
125 logger.Fatal(err)
126 }
127 return client
128}