all repos — fixyoutube-go @ 1ed34f89fd58b854daee23c3e8188e071ff5a852

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

invidious/invidious.go (view raw)

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