all repos — fixyoutube-go @ 48d10f3f94a07a0491c6dbd2bab6b28316162e66

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
 16const maxSizeMB = 50
 17const instancesEndpoint = "https://api.invidious.io/instances.json?sort_by=api,type"
 18const videosEndpoint = "https://%s/api/v1/videos/%s?fields=videoId,title,description,author,lengthSeconds,size,formatStreams"
 19
 20var timeToLive, _ = time.ParseDuration("6h")
 21
 22type Client struct {
 23	http     *http.Client
 24	Instance string
 25}
 26
 27type Format struct {
 28	VideoId   string
 29	Name      string `json:"qualityLabel"`
 30	Height    int
 31	Width     int
 32	Url       string `json:"url"`
 33	Container string `json:"container"`
 34	Size      string `json:"size"`
 35}
 36
 37type Video struct {
 38	VideoId     string   `json:"videoId"`
 39	Title       string   `json:"title"`
 40	Description string   `json:"description"`
 41	Uploader    string   `json:"author"`
 42	Duration    int      `json:"lengthSeconds"`
 43	Formats     []Format `json:"formatStreams"`
 44	Timestamp   time.Time
 45	FormatIndex int
 46}
 47
 48func filter[T any](ss []T, test func(T) bool) (ret []T) {
 49	for _, s := range ss {
 50		if test(s) {
 51			ret = append(ret, s)
 52		}
 53	}
 54	return
 55}
 56
 57func parseOrZero(number string) int {
 58	res, err := strconv.Atoi(number)
 59	if err != nil {
 60		return 0
 61	}
 62	return res
 63}
 64
 65func (c *Client) fetchVideo(videoId string) (*Video, error) {
 66	if c.Instance == "" {
 67		err := c.NewInstance()
 68		if err != nil {
 69			log.Fatal(err, "Could not get a new instance.")
 70			os.Exit(1)
 71		}
 72	}
 73	endpoint := fmt.Sprintf(videosEndpoint, c.Instance, url.QueryEscape(videoId))
 74	resp, err := c.http.Get(endpoint)
 75	if err != nil {
 76		return nil, err
 77	}
 78	defer resp.Body.Close()
 79
 80	body, err := io.ReadAll(resp.Body)
 81	if err != nil {
 82		return nil, err
 83	}
 84
 85	if resp.StatusCode != http.StatusOK {
 86		return nil, fmt.Errorf(string(body))
 87	}
 88
 89	res := &Video{}
 90	err = json.Unmarshal(body, res)
 91	if err != nil {
 92		return nil, err
 93	}
 94
 95	mp4Test := func(f Format) bool { return f.Container == "mp4" }
 96	res.Formats = filter(res.Formats, mp4Test)
 97
 98	for _, f := range res.Formats {
 99		s := strings.Split(f.Size, "x")
100		f.Width = parseOrZero(s[0])
101		f.Height = parseOrZero(s[1])
102	}
103
104	return res, err
105}
106
107func (c *Client) GetVideo(videoId string) (*Video, error) {
108	log.Println("Video", videoId, "was requested.")
109
110	video, err := GetVideoDB(videoId)
111	if err == nil {
112		log.Println("Found a valid cache entry.")
113		return video, nil
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 (c *Client) ProxyVideo(w http.ResponseWriter, videoId string, formatIndex int) error {
160	video, err := GetVideoDB(videoId)
161	if err != nil {
162		http.Error(w, "Bad Request", http.StatusBadRequest)
163		return err
164	}
165
166	fmtAmount := len(video.Formats)
167	idx := formatIndex % fmtAmount
168	url := video.Formats[fmtAmount-1-idx].Url
169	req, err := http.NewRequest(http.MethodGet, url, nil)
170	if err != nil {
171		log.Fatal(err)
172		new_video, err := c.fetchVideo(videoId)
173		if err != nil {
174			log.Fatal("Url for", videoId, "expired:", err)
175			return err
176		}
177		return c.ProxyVideo(w, new_video.VideoId, formatIndex)
178	}
179
180	req.Header.Add("Range", fmt.Sprintf("bytes=0-%d000000", maxSizeMB))
181	resp, err := c.http.Do(req)
182	if err != nil {
183		log.Fatal(err)
184		http.Error(w, err.Error(), http.StatusInternalServerError)
185		return err
186	}
187	defer resp.Body.Close()
188
189	w.Header().Set("content-type", "video/mp4")
190	w.Header().Set("Status", "200")
191
192	_, err = io.Copy(w, resp.Body)
193	if err == nil { // done
194		return nil
195	}
196
197	newIndex := formatIndex + 1
198	if newIndex < fmtAmount {
199		return c.ProxyVideo(w, videoId, newIndex)
200	}
201
202	return err
203}
204
205func NewClient(httpClient *http.Client) *Client {
206	InitDB()
207	return &Client{httpClient, ""}
208}