all repos — fixyoutube-go @ a2de1ac53930095b77cb2248757e24c5624f30ff

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	"regexp"
 12	"strconv"
 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 expireRegex = regexp.MustCompile(`(?i)expire=(\d+)`)
 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	Expire      time.Time
 46	FormatIndex int
 47}
 48
 49func filter[T any](ss []T, test func(T) bool) (ret []T) {
 50	for _, s := range ss {
 51		if test(s) {
 52			ret = append(ret, s)
 53		}
 54	}
 55	return
 56}
 57
 58func parseOrZero(number string) int {
 59	res, err := strconv.Atoi(number)
 60	if err != nil {
 61		return 0
 62	}
 63	return res
 64}
 65
 66func (c *Client) fetchVideo(videoId string) (*Video, error) {
 67	if c.Instance == "" {
 68		err := c.NewInstance()
 69		if err != nil {
 70			log.Fatal(err, "Could not get a new instance.")
 71			os.Exit(1)
 72		}
 73	}
 74	endpoint := fmt.Sprintf(videosEndpoint, c.Instance, url.QueryEscape(videoId))
 75	resp, err := c.http.Get(endpoint)
 76	if err != nil {
 77		return nil, err
 78	}
 79	defer resp.Body.Close()
 80
 81	body, err := io.ReadAll(resp.Body)
 82	if err != nil {
 83		return nil, err
 84	}
 85
 86	if resp.StatusCode != http.StatusOK {
 87		return nil, fmt.Errorf(string(body))
 88	}
 89
 90	res := &Video{}
 91	err = json.Unmarshal(body, res)
 92	if err != nil {
 93		return nil, err
 94	}
 95
 96	mp4Test := func(f Format) bool { return f.Container == "mp4" }
 97	res.Formats = filter(res.Formats, mp4Test)
 98
 99	expireString := expireRegex.FindStringSubmatch(res.Formats[0].Url)
100	expireTimestamp, err := strconv.ParseInt(expireString[1], 10, 64)
101	if err != nil {
102		fmt.Println("Error:", err)
103		return nil, err
104	}
105	res.Expire = time.Unix(expireTimestamp, 0)
106
107	return res, err
108}
109
110func (c *Client) GetVideo(videoId string) (*Video, error) {
111	log.Println("Video", videoId, "was requested.")
112
113	video, err := GetVideoDB(videoId)
114	if err == nil {
115		log.Println("Found a valid cache entry.")
116		return video, nil
117	}
118
119	video, err = c.fetchVideo(videoId)
120
121	if err != nil {
122		if err.Error() == "{}" {
123			return nil, err
124		}
125		log.Println(err)
126		err = c.NewInstance()
127		if err != nil {
128			log.Fatal("Could not get a new instance: ", err)
129			time.Sleep(10 * time.Second)
130		}
131		return c.GetVideo(videoId)
132	}
133	log.Println("Retrieved by API.")
134
135	CacheVideoDB(*video)
136	return video, nil
137}
138
139func (c *Client) NewInstance() error {
140	resp, err := c.http.Get(instancesEndpoint)
141	if err != nil {
142		return err
143	}
144	defer resp.Body.Close()
145
146	body, err := io.ReadAll(resp.Body)
147	if err != nil {
148		return err
149	}
150
151	if resp.StatusCode != http.StatusOK {
152		return fmt.Errorf(string(body))
153	}
154
155	var jsonArray [][]interface{}
156	err = json.Unmarshal(body, &jsonArray)
157	if err != nil {
158		return err
159	}
160
161	c.Instance = jsonArray[0][0].(string)
162	log.Println("Using new instance:", c.Instance)
163	return nil
164}
165
166func (c *Client) ProxyVideo(w http.ResponseWriter, videoId string, formatIndex int) error {
167	video, err := GetVideoDB(videoId)
168	if err != nil {
169		http.Error(w, "Bad Request", http.StatusBadRequest)
170		return err
171	}
172
173	fmtAmount := len(video.Formats)
174	idx := formatIndex % fmtAmount
175	url := video.Formats[fmtAmount-1-idx].Url
176	req, err := http.NewRequest(http.MethodGet, url, nil)
177	if err != nil {
178		log.Fatal(err)
179		new_video, err := c.fetchVideo(videoId)
180		if err != nil {
181			log.Fatal("Url for", videoId, "expired:", err)
182			return err
183		}
184		return c.ProxyVideo(w, new_video.VideoId, formatIndex)
185	}
186
187	req.Header.Add("Range", fmt.Sprintf("bytes=0-%d000000", maxSizeMB))
188	resp, err := c.http.Do(req)
189	if err != nil {
190		log.Fatal(err)
191		http.Error(w, err.Error(), http.StatusInternalServerError)
192		return err
193	}
194	defer resp.Body.Close()
195
196	w.Header().Set("content-type", "video/mp4")
197	w.Header().Set("Status", "200")
198
199	_, err = io.Copy(w, resp.Body)
200	if err == nil { // done
201		return nil
202	}
203
204	newIndex := formatIndex + 1
205	if newIndex < fmtAmount {
206		return c.ProxyVideo(w, videoId, newIndex)
207	}
208
209	return err
210}
211
212func NewClient(httpClient *http.Client) *Client {
213	InitDB()
214	return &Client{httpClient, ""}
215}