all repos — fixyoutube-go @ aacf9865a2bfb0355680dd4e28b98e19d2b8400a

A better way to embed YouTube videos everywhere (inspired by FixTweet).

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}