all repos — fixyoutube-go @ 41c1328a62c74e053854ea764338190a57735988

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	idx := formatIndex % len(video.Formats)
167	url := video.Formats[len(video.Formats)-1-idx].Url
168	req, err := http.NewRequest(http.MethodGet, url, nil)
169	if err != nil {
170		log.Fatal(err)
171		new_video, err := c.fetchVideo(videoId)
172		if err != nil {
173			log.Fatal("Url for", videoId, "expired:", err)
174			return err
175		}
176		return c.ProxyVideo(w, new_video.VideoId, formatIndex)
177	}
178
179	req.Header.Add("Range", fmt.Sprintf("bytes=0-%d000000", maxSizeMB))
180	resp, err := c.http.Do(req)
181	if err != nil {
182		log.Fatal(err)
183		http.Error(w, err.Error(), http.StatusInternalServerError)
184		return err
185	}
186	defer resp.Body.Close()
187
188	w.Header().Set("content-type", "video/mp4")
189	w.Header().Set("Status", "200")
190
191	i, err := io.Copy(w, resp.Body)
192	fmt.Println(i, err)
193	return err
194}
195
196func NewClient(httpClient *http.Client) *Client {
197	InitDB()
198	return &Client{httpClient, ""}
199}