all repos — fixyoutube-go @ 712b9c42dc774930f2aff23a028d98e3f7b993d3

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

invidious/invidious.go (view raw)

  1package invidious
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"io"
  7	"log"
  8	"net/http"
  9	"net/url"
 10	"os"
 11	"strconv"
 12	"strings"
 13	"time"
 14)
 15
 16var instancesEndpoint = "https://api.invidious.io/instances.json?sort_by=api,type"
 17var videosEndpoint = "https://%s/api/v1/videos/%s?fields=videoId,title,description,author,lengthSeconds,size,formatStreams"
 18var timeToLive, _ = time.ParseDuration("6h")
 19
 20type Client struct {
 21	http     *http.Client
 22	Instance string
 23}
 24
 25type Format struct {
 26	Url       string `json:"url"`
 27	Container string `json:"container"`
 28	Size      string `json:"size"`
 29}
 30
 31type Video struct {
 32	VideoId       string   `json:"videoId"`
 33	Title         string   `json:"title"`
 34	Description   string   `json:"description"`
 35	Uploader      string   `json:"author"`
 36	Duration      int      `json:"lengthSeconds"`
 37	FormatStreams []Format `json:"formatStreams"`
 38	Url           string
 39	Height        int
 40	Width         int
 41	Timestamp     time.Time
 42}
 43
 44func filter[T any](ss []T, test func(T) bool) (ret []T) {
 45	for _, s := range ss {
 46		if test(s) {
 47			ret = append(ret, s)
 48		}
 49	}
 50	return
 51}
 52
 53func parseOrZero(number string) int {
 54	res, err := strconv.Atoi(number)
 55	if err != nil {
 56		return 0
 57	}
 58	return res
 59}
 60
 61func (c *Client) FetchVideo(videoId string) (*Video, error) {
 62	if c.Instance == "" {
 63		err := c.NewInstance()
 64		if err != nil {
 65			log.Fatal(err, "Could not get a new instance.")
 66			os.Exit(1)
 67		}
 68	}
 69	endpoint := fmt.Sprintf(videosEndpoint, c.Instance, url.QueryEscape(videoId))
 70	resp, err := c.http.Get(endpoint)
 71	if err != nil {
 72		return nil, err
 73	}
 74	defer resp.Body.Close()
 75
 76	body, err := io.ReadAll(resp.Body)
 77	if err != nil {
 78		return nil, err
 79	}
 80
 81	if resp.StatusCode != http.StatusOK {
 82		return nil, fmt.Errorf(string(body))
 83	}
 84
 85	res := &Video{}
 86	err = json.Unmarshal(body, res)
 87	if err != nil {
 88		return nil, err
 89	}
 90
 91	mp4Test := func(f Format) bool { return f.Container == "mp4" }
 92	mp4Formats := filter(res.FormatStreams, mp4Test)
 93	myFormat := mp4Formats[len(mp4Formats)-1]
 94	mySize := strings.Split(myFormat.Size, "x")
 95
 96	res.Url = myFormat.Url
 97	res.Width = parseOrZero(mySize[0])
 98	res.Height = parseOrZero(mySize[1])
 99
100	return res, err
101}
102
103func (c *Client) GetVideo(videoId string) (*Video, error) {
104	log.Println("Video", videoId, "was requested.")
105
106	video, err := GetVideoDB(videoId)
107	if err == nil {
108		now := time.Now()
109		delta := now.Sub(video.Timestamp)
110		if delta < timeToLive {
111			log.Println("Found a valid cache entry from", delta, "ago.")
112			return video, nil
113		}
114	}
115
116	video, err = c.FetchVideo(videoId)
117	if err != nil {
118		log.Println(err)
119		err = c.NewInstance()
120		if err != nil {
121			log.Fatal("Could not get a new instance: ", err)
122			time.Sleep(10)
123		}
124		return c.GetVideo(videoId)
125	}
126	log.Println("Retrieved by API.")
127
128	CacheVideoDB(*video)
129	return video, nil
130}
131
132func (c *Client) NewInstance() error {
133	resp, err := c.http.Get(instancesEndpoint)
134	if err != nil {
135		return err
136	}
137	defer resp.Body.Close()
138
139	body, err := io.ReadAll(resp.Body)
140	if err != nil {
141		return err
142	}
143
144	if resp.StatusCode != http.StatusOK {
145		return fmt.Errorf(string(body))
146	}
147
148	var jsonArray [][]interface{}
149	err = json.Unmarshal(body, &jsonArray)
150	if err != nil {
151		return err
152	}
153
154	c.Instance = jsonArray[0][0].(string)
155	log.Println("Using new instance:", c.Instance)
156	return nil
157}
158
159func NewClient(httpClient *http.Client) *Client {
160	InitDB()
161	return &Client{httpClient, ""}
162}