all repos — fixyoutube-go @ dfabe54c50cb40c8ffbe27dd08c2c0da0d8055b4

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

major code cleanup
Marco Andronaco andronacomarco@gmail.com
Sat, 13 Jan 2024 12:03:03 +0100
commit

dfabe54c50cb40c8ffbe27dd08c2c0da0d8055b4

parent

f2f8edfc003d89ede8c1246a415921d151be7123

5 files changed, 170 insertions(+), 159 deletions(-)

jump to
M fixyoutube.gofixyoutube.go

@@ -155,10 +155,10 @@ url := video.Formats[i].Url

b, l, httpStatus := invidiousClient.ProxyVideo(url, formatIndex) switch httpStatus { case http.StatusOK: - header := w.Header() - header.Set("Status", "200") - header.Set("Content-Type", "video/mp4") - header.Set("Content-Length", strconv.FormatInt(l, 10)) + h := w.Header() + h.Set("Status", "200") + h.Set("Content-Type", "video/mp4") + h.Set("Content-Length", strconv.FormatInt(l, 10)) io.Copy(w, b) return case http.StatusBadRequest:

@@ -173,7 +173,7 @@ }

} func main() { - logger.SetLevel(logrus.DebugLevel) + //logger.SetLevel(logrus.DebugLevel) err := godotenv.Load() if err != nil { logger.Info("No .env file provided.")

@@ -201,7 +201,7 @@ r.HandleFunc("/proxy/{videoId}/{formatIndex}", proxyHandler(videoapi))

r.HandleFunc("/{videoId}", shortHandler(videoapi)) r.HandleFunc("/{videoId}/{formatIndex}", shortHandler(videoapi)) /* - // native go implementation (useless until february 2024) + // native go implementation r := http.NewServeMux() r.HandleFunc("/watch", watchHandler(videoapi)) r.HandleFunc("/{videoId}/", shortHandler(videoapi))
A invidious/api.go

@@ -0,0 +1,104 @@

+package invidious + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "time" +) + +func (c *Client) fetchVideo(videoId string) (*Video, int) { + endpoint := fmt.Sprintf(videosEndpoint, c.Instance, url.QueryEscape(videoId)) + resp, err := c.http.Get(endpoint) + if err != nil { + logger.Error(err) + return nil, http.StatusInternalServerError + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + logger.Warn("Invidious gave the following status code: ", resp.StatusCode) + return nil, http.StatusNotFound + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + logger.Error(err) + return nil, http.StatusInternalServerError + } + + res := &Video{} + err = json.Unmarshal(body, res) + if err != nil { + logger.Error(err) + return nil, http.StatusInternalServerError + } + + mp4Test := func(f Format) bool { return f.Container == "mp4" } + res.Formats = filter(res.Formats, mp4Test) + + expireString := expireRegex.FindStringSubmatch(res.Formats[0].Url) + expireTimestamp, err := strconv.ParseInt(expireString[1], 10, 64) + if err != nil { + logger.Error(err) + return nil, http.StatusInternalServerError + } + res.Expire = time.Unix(expireTimestamp, 0) + return res, http.StatusOK +} + +func (c *Client) isNotTimedOut(instance string) bool { + for i := range c.timeouts { + cur := c.timeouts[i] + if instance == cur.Instance { + return false + } + } + return true +} + +func (c *Client) NewInstance() error { + now := time.Now() + + timeoutsTest := func(t Timeout) bool { return now.Sub(t.Timestamp) < timeoutDuration } + c.timeouts = filter(c.timeouts, timeoutsTest) + c.timeouts = append(c.timeouts, Timeout{c.Instance, now}) + + resp, err := c.http.Get(instancesEndpoint) + if err != nil { + return err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return HTTPError{resp.StatusCode} + } + + var jsonArray [][]interface{} + err = json.Unmarshal(body, &jsonArray) + if err != nil { + logger.Error("Could not unmarshal JSON response for instances.") + return err + } + + for i := range jsonArray { + instance := jsonArray[i][0].(string) + instanceTest := func(t Timeout) bool { return t.Instance == instance } + result := filter(c.timeouts, instanceTest) + if len(result) == 0 { + c.Instance = instance + logger.Info("Using new instance: ", c.Instance) + return nil + } + } + + return fmt.Errorf("Cannot find a valid instance.") +}
M invidious/cache.goinvidious/cache.go

@@ -41,28 +41,28 @@ defer db.Close()

cacheVideo, err := db.Prepare(cacheVideoQuery) if err != nil { - logger.Error("Could not cache video:", err) + logger.Error("Could not cache video: ", err) return err } defer cacheVideo.Close() _, err = cacheVideo.Exec(v.VideoId, v.Title, v.Description, v.Uploader, v.Duration, v.Expire) if err != nil { - logger.Error("Could not cache video:", err) + logger.Error("Could not cache video: ", err) return err } for _, f := range v.Formats { cacheFormat, err := db.Prepare(cacheFormatQuery) if err != nil { - logger.Error("Could not cache format:", err) + logger.Error("Could not cache format: ", err) return err } defer cacheVideo.Close() _, err = cacheFormat.Exec(v.VideoId, f.Name, f.Height, f.Width, f.Url) if err != nil { - logger.Error("Could not cache format:", err) + logger.Error("Could not cache format: ", err) return err } }

@@ -75,7 +75,7 @@ defer db.Close()

getVideo, err := db.Prepare(getVideoQuery) if err != nil { - logger.Error("Could not get video:", err) + logger.Error("Could not get video: ", err) return nil, err } defer getVideo.Close()

@@ -94,14 +94,14 @@ }

getFormat, err := db.Prepare(getFormatQuery) if err != nil { - logger.Error("Could not get format:", err) + logger.Error("Could not get format: ", err) return nil, err } defer getFormat.Close() response, err := getFormat.Query(videoId) if err != nil { - logger.Error("Could not get formats:", err) + logger.Error("Could not get formats: ", err) return nil, err } defer response.Close()

@@ -110,7 +110,7 @@ for response.Next() {

f := Format{} err := response.Scan(&f.VideoId, &f.Name, &f.Height, &f.Width, &f.Url) if err != nil { - logger.Error("Could not get formats:", err) + logger.Error("Could not get formats: ", err) return nil, err } v.Formats = append(v.Formats, f)
M invidious/invidious.goinvidious/invidious.go

@@ -1,12 +1,8 @@

package invidious import ( - "bytes" - "encoding/json" "fmt" - "io" "net/http" - "net/url" "regexp" "strconv" "time"

@@ -80,48 +76,6 @@ func (e HTTPError) Error() string {

return fmt.Sprintf("HTTP error: %d", e.StatusCode) } -func (c *Client) fetchVideo(videoId string) (*Video, error) { - if c.Instance == "" { - err := c.NewInstance() - if err != nil { - logger.Fatal(err, "Could not get a new instance.") - } - } - endpoint := fmt.Sprintf(videosEndpoint, c.Instance, url.QueryEscape(videoId)) - resp, err := c.http.Get(endpoint) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - if resp.StatusCode != http.StatusOK { - return nil, HTTPError{resp.StatusCode} - } - - res := &Video{} - err = json.Unmarshal(body, res) - if err != nil { - return nil, err - } - - mp4Test := func(f Format) bool { return f.Container == "mp4" } - res.Formats = filter(res.Formats, mp4Test) - - expireString := expireRegex.FindStringSubmatch(res.Formats[0].Url) - expireTimestamp, err := strconv.ParseInt(expireString[1], 10, 64) - if err != nil { - fmt.Println("Error:", err) - return nil, err - } - res.Expire = time.Unix(expireTimestamp, 0) - return res, nil -} - func (c *Client) GetVideo(videoId string, fromCache bool) (*Video, error) { logger.Info("Video https://youtu.be/", videoId, " was requested.")

@@ -136,20 +90,18 @@ return video, nil

} } - video, err = c.fetchVideo(videoId) + video, httpErr := c.fetchVideo(videoId) - if err != nil { - if httpErr, ok := err.(HTTPError); ok { - // handle HTTPError - s := httpErr.StatusCode - if s == http.StatusNotFound || s == http.StatusInternalServerError { - logger.Debug("Video does not exist.") - return nil, err - } - logger.Debug("Invidious HTTP error: ", httpErr.StatusCode) - } - // handle generic error - logger.Error(err) + switch httpErr { + case http.StatusOK: + logger.Info("Retrieved by API.") + break + case http.StatusNotFound: + logger.Debug("Video does not exist or can't be retrieved.") + return nil, err + default: + fallthrough + case http.StatusInternalServerError: err = c.NewInstance() if err != nil { logger.Error("Could not get a new instance: ", err)

@@ -157,7 +109,6 @@ time.Sleep(10 * time.Second)

} return c.GetVideo(videoId, true) } - logger.Info("Retrieved by API.") err = CacheVideoDB(*video) if err != nil {

@@ -167,95 +118,16 @@ }

return video, nil } -func (c *Client) isNotTimedOut(instance string) bool { - for i := range c.timeouts { - cur := c.timeouts[i] - if instance == cur.Instance { - return false - } - } - return true -} - -func (c *Client) NewInstance() error { - now := time.Now() - - timeoutsTest := func(t Timeout) bool { return now.Sub(t.Timestamp) < timeoutDuration } - c.timeouts = filter(c.timeouts, timeoutsTest) - - timeout := Timeout{c.Instance, now} - c.timeouts = append(c.timeouts, timeout) - - resp, err := c.http.Get(instancesEndpoint) - if err != nil { - return err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - - if resp.StatusCode != http.StatusOK { - return HTTPError{resp.StatusCode} - } - - var jsonArray [][]interface{} - err = json.Unmarshal(body, &jsonArray) - if err != nil { - logger.Error("Could not unmarshal JSON response for instances.") - return err - } - - for i := range jsonArray { - instance := jsonArray[i][0].(string) - instanceTest := func(t Timeout) bool { return t.Instance == instance } - result := filter(c.timeouts, instanceTest) - if len(result) == 0 { - c.Instance = instance - logger.Info("Using new instance: ", c.Instance) - return nil - } - } - logger.Error("Cannot find a valid instance.") - return err -} - -func (c *Client) ProxyVideo(url string, formatIndex int) (*bytes.Buffer, int64, int) { - - req, err := http.NewRequest(http.MethodGet, url, nil) - if err != nil { - logger.Error(err) // bad request - return nil, 0, http.StatusInternalServerError - } - - resp, err := c.http.Do(req) - if err != nil { - logger.Error(err) // request failed - return nil, 0, http.StatusGone - } - - if resp.ContentLength > maxSizeBytes { - logger.Debug("Format ", formatIndex, ": Content-Length exceeds max size.") - return nil, 0, http.StatusBadRequest - } - defer resp.Body.Close() - - b := new(bytes.Buffer) - l, err := io.Copy(b, resp.Body) - if l != resp.ContentLength { - return nil, 0, http.StatusBadRequest - } - - return b, l, http.StatusOK -} - func NewClient(httpClient *http.Client) *Client { InitDB() - return &Client{ + client := &Client{ http: httpClient, timeouts: []Timeout{}, Instance: "", } + err := client.NewInstance() + if err != nil { + logger.Fatal(err) + } + return client }
A invidious/proxy.go

@@ -0,0 +1,35 @@

+package invidious + +import ( + "bytes" + "io" + "net/http" +) + +func (c *Client) ProxyVideo(url string, formatIndex int) (*bytes.Buffer, int64, int) { + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + logger.Error(err) // bad request + return nil, 0, http.StatusInternalServerError + } + + resp, err := c.http.Do(req) + if err != nil { + logger.Error(err) // request failed + return nil, 0, http.StatusGone + } + + if resp.ContentLength > maxSizeBytes { + logger.Debug("Format ", formatIndex, ": Content-Length exceeds max size.") + return nil, 0, http.StatusBadRequest + } + defer resp.Body.Close() + + b := new(bytes.Buffer) + l, err := io.Copy(b, resp.Body) + if l != resp.ContentLength { + return nil, 0, http.StatusBadRequest + } + + return b, l, http.StatusOK +}