all repos — fixyoutube-go @ 5f9d33f75666cde5e6b40a48e034d0d9e6ade442

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