all repos — fixyoutube-go @ 885bf1a0eb716d6fe63a385dc1bc5164564d4cf0

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	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	Duration    int      `json:"lengthSeconds"`
 45	Formats     []Format `json:"formatStreams"`
 46	Expire      time.Time
 47	Url         string
 48}
 49
 50func filter[T any](ss []T, test func(T) bool) (ret []T) {
 51	for _, s := range ss {
 52		if test(s) {
 53			ret = append(ret, s)
 54		}
 55	}
 56	return
 57}
 58
 59func NewVideoBuffer(b *bytes.Buffer, l int64) *VideoBuffer {
 60	d := new(bytes.Buffer)
 61	d.Write(b.Bytes())
 62
 63	return &VideoBuffer{
 64		Buffer: d,
 65		Length: l,
 66	}
 67}
 68
 69func (vb *VideoBuffer) Clone() *VideoBuffer {
 70	return NewVideoBuffer(vb.Buffer, vb.Length)
 71}
 72
 73func (vb *VideoBuffer) ValidateLength() bool {
 74	return vb.Length > 0 && vb.Length == int64(vb.Buffer.Len())
 75}
 76
 77func (c *Client) GetVideo(videoId string, fromCache bool) (*Video, error) {
 78	logger.Info("Video https://youtu.be/", videoId, " was requested.")
 79
 80	var video *Video
 81	var err error
 82
 83	if fromCache {
 84		video, err = GetVideoDB(videoId)
 85		if err == nil {
 86			logger.Info("Found a valid cache entry.")
 87			return video, nil
 88		}
 89	}
 90
 91	video, httpErr := c.fetchVideo(videoId)
 92
 93	switch httpErr {
 94	case http.StatusOK:
 95		logger.Info("Retrieved by API.")
 96	case http.StatusNotFound:
 97		logger.Debug("Video does not exist or can't be retrieved.")
 98		return nil, err
 99	default:
100		err = c.NewInstance()
101		if err != nil {
102			logger.Error("Could not get a new instance: ", err)
103			time.Sleep(10 * time.Second)
104		}
105		return c.GetVideo(videoId, true)
106	}
107
108	err = CacheVideoDB(*video)
109	if err != nil {
110		logger.Warn("Could not cache video id: ", videoId)
111		logger.Warn(err)
112	}
113	return video, nil
114}
115
116func NewClient(httpClient *http.Client, options ClientOptions) *Client {
117	InitDB()
118	timeouts := volatile.NewVolatile[string, error](options.TimeoutDuration, options.CleanupInterval)
119	buffers := volatile.NewVolatile[string, VideoBuffer](options.CacheDuration, options.CleanupInterval)
120	client := &Client{
121		http:     httpClient,
122		timeouts: timeouts,
123		buffers:  buffers,
124		Options:  options,
125	}
126	err := client.NewInstance()
127	if err != nil {
128		logger.Fatal(err)
129	}
130	return client
131}