all repos — fixyoutube-go @ e3cc3478bdf01404778bdca36a94a9abb73d74d8

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")
 19var maxSizeMB = 50
 20
 21type Client struct {
 22	http     *http.Client
 23	Instance string
 24}
 25
 26type Format struct {
 27	Url       string `json:"url"`
 28	Container string `json:"container"`
 29	Size      string `json:"size"`
 30}
 31
 32type Video struct {
 33	VideoId       string   `json:"videoId"`
 34	Title         string   `json:"title"`
 35	Description   string   `json:"description"`
 36	Uploader      string   `json:"author"`
 37	Duration      int      `json:"lengthSeconds"`
 38	FormatStreams []Format `json:"formatStreams"`
 39	Url           string
 40	Height        int
 41	Width         int
 42	Timestamp     time.Time
 43}
 44
 45func filter[T any](ss []T, test func(T) bool) (ret []T) {
 46	for _, s := range ss {
 47		if test(s) {
 48			ret = append(ret, s)
 49		}
 50	}
 51	return
 52}
 53
 54func parseOrZero(number string) int {
 55	res, err := strconv.Atoi(number)
 56	if err != nil {
 57		return 0
 58	}
 59	return res
 60}
 61
 62func (c *Client) fetchVideo(videoId string) (*Video, error) {
 63	if c.Instance == "" {
 64		err := c.NewInstance()
 65		if err != nil {
 66			log.Fatal(err, "Could not get a new instance.")
 67			os.Exit(1)
 68		}
 69	}
 70	endpoint := fmt.Sprintf(videosEndpoint, c.Instance, url.QueryEscape(videoId))
 71	resp, err := c.http.Get(endpoint)
 72	if err != nil {
 73		return nil, err
 74	}
 75	defer resp.Body.Close()
 76
 77	body, err := io.ReadAll(resp.Body)
 78	if err != nil {
 79		return nil, err
 80	}
 81
 82	if resp.StatusCode != http.StatusOK {
 83		return nil, fmt.Errorf(string(body))
 84	}
 85
 86	res := &Video{}
 87	err = json.Unmarshal(body, res)
 88	if err != nil {
 89		return nil, err
 90	}
 91
 92	mp4Test := func(f Format) bool { return f.Container == "mp4" }
 93	mp4Formats := filter(res.FormatStreams, mp4Test)
 94	myFormat := mp4Formats[len(mp4Formats)-1]
 95	mySize := strings.Split(myFormat.Size, "x")
 96
 97	res.Url = myFormat.Url
 98	res.Width = parseOrZero(mySize[0])
 99	res.Height = parseOrZero(mySize[1])
100
101	return res, err
102}
103
104func (c *Client) GetVideo(videoId string) (*Video, error) {
105	log.Println("Video", videoId, "was requested.")
106
107	video, err := GetVideoDB(videoId)
108	if err == nil {
109		now := time.Now()
110		delta := now.Sub(video.Timestamp)
111		if delta < timeToLive {
112			log.Println("Found a valid cache entry from", delta, "ago.")
113			return video, nil
114		}
115	}
116
117	video, err = c.fetchVideo(videoId)
118	if err != nil {
119		log.Println(err)
120		err = c.NewInstance()
121		if err != nil {
122			log.Fatal("Could not get a new instance: ", err)
123			time.Sleep(10)
124		}
125		return c.GetVideo(videoId)
126	}
127	log.Println("Retrieved by API.")
128
129	CacheVideoDB(*video)
130	return video, nil
131}
132
133func (c *Client) NewInstance() error {
134	resp, err := c.http.Get(instancesEndpoint)
135	if err != nil {
136		return err
137	}
138	defer resp.Body.Close()
139
140	body, err := io.ReadAll(resp.Body)
141	if err != nil {
142		return err
143	}
144
145	if resp.StatusCode != http.StatusOK {
146		return fmt.Errorf(string(body))
147	}
148
149	var jsonArray [][]interface{}
150	err = json.Unmarshal(body, &jsonArray)
151	if err != nil {
152		return err
153	}
154
155	c.Instance = jsonArray[0][0].(string)
156	log.Println("Using new instance:", c.Instance)
157	return nil
158}
159
160func (c *Client) ProxyVideo(videoId string, w http.ResponseWriter) error {
161	video, err := GetVideoDB(videoId)
162	if err != nil {
163		http.Error(w, "Bad Request", http.StatusBadRequest)
164		return err
165	}
166
167	req, err := http.NewRequest(http.MethodGet, video.Url, nil)
168	if err != nil {
169		log.Fatal(err)
170		new_video, err := c.fetchVideo(videoId)
171		if err != nil {
172			log.Fatal("Url for", videoId, "expired:", err)
173			return err
174		}
175		return c.ProxyVideo(new_video.VideoId, w)
176	}
177
178	req.Header.Add("Range", fmt.Sprintf("bytes=0-%d000000", maxSizeMB))
179	resp, err := c.http.Do(req)
180	if err != nil {
181		log.Fatal(err)
182		http.Error(w, err.Error(), http.StatusInternalServerError)
183		return err
184	}
185	defer resp.Body.Close()
186
187	w.Header().Set("content-type", "video/mp4")
188	w.Header().Set("Status", "206") // Partial Content
189
190	i, err := io.Copy(w, resp.Body)
191	fmt.Println(i, err)
192	return err
193}
194
195func NewClient(httpClient *http.Client) *Client {
196	InitDB()
197	return &Client{httpClient, ""}
198}