all repos — fixyoutube-go @ 558f876a7938e8fde5bbf84dfe77dde6b7669634

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

initial commit
Andronaco Marco marco.andronaco@olivetti.com
Mon, 18 Dec 2023 21:40:59 +0100
commit

558f876a7938e8fde5bbf84dfe77dde6b7669634

A .gitignore

@@ -0,0 +1,2 @@

+.env +__debug_bin*
A .vscode/launch.json

@@ -0,0 +1,15 @@

+{ + // Usare IntelliSense per informazioni sui possibili attributi. + // Al passaggio del mouse vengono visualizzate le descrizioni degli attributi esistenti. + // Per altre informazioni, visitare: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "." + } + ] +}
A LICENSE

@@ -0,0 +1,21 @@

+MIT License + +Copyright (c) 2023 Marco Andronaco + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.
A README.md

@@ -0,0 +1,14 @@

+# FixYouTube +Embed YouTube videos on Telegram, Discord and more! + +## How to use: +Replace `www.youtube.com` or `youtu.be` with `y.outube.duckdns.org` to fix embeds for short videos. + +https://github.com/BiRabittoh/FixYouTube/assets/26506860/e1ad5397-41c8-4073-9b3e-598c66241255 + +## Instructions + +### Debug +``` +go run . +```
A fixyoutube.go

@@ -0,0 +1,118 @@

+package main + +import ( + "bytes" + "html/template" + "log" + "net/http" + "net/url" + "os" + "slices" + "time" + + "github.com/BiRabittoh/fixyoutube-go/invidious" + "github.com/gorilla/mux" + "github.com/joho/godotenv" +) + +var templatesDirectory = "templates/" +var indexTemplate = template.Must(template.ParseFiles(templatesDirectory + "index.html")) +var videoTemplate = template.Must(template.ParseFiles(templatesDirectory + "video.html")) + +func indexHandler(w http.ResponseWriter, r *http.Request) { + buf := &bytes.Buffer{} + err := indexTemplate.Execute(buf, nil) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + buf.WriteTo(w) +} + +func watchHandler(newsapi *invidious.Client) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + u, err := url.Parse(r.URL.String()) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + params := u.Query() + videoId := params.Get("v") + + video, err := newsapi.FetchEverything(videoId) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + buf := &bytes.Buffer{} + err = videoTemplate.Execute(buf, video) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + buf.WriteTo(w) + } +} + +var blacklist = []string{"favicon.ico", "robots.txt"} + +func shortHandler(newsapi *invidious.Client) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + videoId := mux.Vars(r)["videoId"] + + if slices.Contains(blacklist, videoId) { + http.Error(w, "Not a valid ID.", http.StatusBadRequest) + return + } + + video, err := newsapi.FetchEverything(videoId) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + buf := &bytes.Buffer{} + err = videoTemplate.Execute(buf, video) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + buf.WriteTo(w) + } +} + +func main() { + err := godotenv.Load() + if err != nil { + log.Println("Error loading .env file") + } + + port := os.Getenv("PORT") + if port == "" { + port = "3000" + } + + myClient := &http.Client{Timeout: 10 * time.Second} + videoapi := invidious.NewClient(myClient, "y.birabittoh.duckdns.org") + + r := mux.NewRouter() + r.HandleFunc("/", indexHandler) + r.HandleFunc("/watch", watchHandler(videoapi)) + r.HandleFunc("/{videoId}", shortHandler(videoapi)) + //r.HandleFunc("/proxy/{videoId}", proxyHandler) + + /* + // native go implementation (useless until february 2024) + r := http.NewServeMux() + r.HandleFunc("/watch", watchHandler(videoapi)) + r.HandleFunc("/{videoId}/", shortHandler(videoapi)) + r.HandleFunc("/", indexHandler) + */ + println("Serving on port", port) + http.ListenAndServe(":"+port, r) +}
A go.mod

@@ -0,0 +1,7 @@

+module github.com/BiRabittoh/fixyoutube-go + +go 1.21.5 + +require github.com/joho/godotenv v1.5.1 + +require github.com/gorilla/mux v1.8.1
A go.sum

@@ -0,0 +1,4 @@

+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
A invidious/invidious.go

@@ -0,0 +1,91 @@

+package invidious + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" +) + +type Client struct { + http *http.Client + Instance string +} + +type Format struct { + Url string `json:"url"` + Container string `json:"container"` + Size string `json:"size"` +} + +type Video struct { + VideoId string `json:"videoId"` + Title string `json:"title"` + Description string `json:"description"` + Uploader string `json:"author"` + Duration int `json:"lengthSeconds"` + FormatStreams []Format `json:"formatStreams"` + Url string + Height int + Width int +} + +func filter[T any](ss []T, test func(T) bool) (ret []T) { + for _, s := range ss { + if test(s) { + ret = append(ret, s) + } + } + return +} + +func parseOrZero(number string) int { + res, err := strconv.Atoi(number) + if err != nil { + return 0 + } + return res +} + +func (c *Client) FetchEverything(videoId string) (*Video, error) { + endpoint := fmt.Sprintf("https://%s/api/v1/videos/%s?fields=videoId,title,description,author,lengthSeconds,size,formatStreams", 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, fmt.Errorf(string(body)) + } + + res := &Video{} + err = json.Unmarshal(body, res) + if err != nil { + return nil, err + } + + mp4Test := func(f Format) bool { return f.Container == "mp4" } + mp4Formats := filter(res.FormatStreams, mp4Test) + myFormat := mp4Formats[len(mp4Formats)-1] + mySize := strings.Split(myFormat.Size, "x") + + res.Url = myFormat.Url + res.Width = parseOrZero(mySize[0]) + res.Height = parseOrZero(mySize[1]) + + return res, err +} + +func NewClient(httpClient *http.Client, instance string) *Client { + return &Client{httpClient, instance} +}
A templates/index.html

@@ -0,0 +1,48 @@

+<!doctype html> +<html lang="en"> + +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta property="og:title" content="FixYouTube" /> + <meta property="og:site_name" content="FixYouTube" /> + <meta property="og:description" content="Embed YouTube videos on Telegram, Discord and more!" /> + <title>FixYouTube</title> + + <link rel="icon" + href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🛠</text></svg>"> + <link rel="stylesheet" href="https://unpkg.com/@picocss/pico@latest/css/pico.min.css"> +</head> + +<body> + <main class="container" style="max-width: 35rem"> + <hgroup> + <h1>FixYouTube</h1> + <h2>Embed YouTube videos on Telegram, Discord and more!</h2> + </hgroup> + <p>FixYouTube serves fixed YouTube video embeds. Heavily inspired by <a href="https://fxtwitter.com">fxtwitter.com</a> and <a href="https://ddinstagram.com">ddinstagram.com</a>.</p> + + <section> + <header> + <h3 style="margin-bottom: 4px">How to Use</h3> + <p>Replace <code>www.youtube.com</code> or <code>youtu.be</code> with <span id="changeme">this domain</span> to fix embeds for short videos.</p> + </header> + <video + src="https://github.com/BiRabittoh/FixYouTube/assets/26506860/2896d39e-a86e-47ce-939a-785b73d11683" + style="width: 100%; max-height: 100%;" autoplay loop muted controls> + Your browser does not support the video tag. + </video> + <hr> + <small><a href="https://github.com/BiRabittoh/FixYouTube" target="_blank">Source code available in GitHub!</a></small> + <br> + <small>• YouTube is a trademark of Google LLC. This app is not affiliated with Google LLC.</small> + </section> + </main> + <script> + code = document.createElement('code'); + code.innerText = window.location.host; + document.getElementById('changeme').replaceWith(code); + </script> +</body> + +</html>
A templates/video.html

@@ -0,0 +1,33 @@

+<!DOCTYPE html> +<html lang="und"> + ███████ ██ ██ ██ ██ ██ ███ ██ ██ ██████ ██ ██ ██████ ███████ + ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + █████ ██ ███ ████ ██ ██ ██ ██ ██ ██ ██ ██████ █████ + ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ + ██ ██ ██ ██ ██ ███ ███ ██ ███ ██████ ███████ + ██ + ██ A better way to embed YouTube videos on Telegram (inspired by FixTweet). + ██ +<head> + <link rel="canonical" href="https://www.youtube.com/watch?v={{ .VideoId }}" /> + <meta property="theme-color" content="0000FF" /> + <meta property="twitter:card" content="player" /> + <meta property="twitter:site" content="{{ .Uploader }}" /> + <meta property="twitter:creator" content="{{ .Uploader }}" /> + <meta property="twitter:title" content="{{ .Title }}" /> + <!--<meta http-equiv="refresh" content="0;url=https://www.youtube.com/watch?v={{ .VideoId }}" />--> + <meta property="og:url" content="https://www.youtube.com/watch?v={{ .VideoId }}" /> + <meta property="og:title" content="{{ .Title }}" /> + <meta property="og:description" content="{{ .Description }}" /> + <meta property="og:site_name" content="FixYouTube ({{ .Uploader }})" /> + <meta property="twitter:image" content="0" /> + <meta property="twitter:player:stream:content_type" content="video/mp4" /> + <meta property="twitter:player:height" content="{{ .Height }}" /> + <meta property="twitter:player:width" content="{{ .Width }}" /> + <meta property="og:video" content="/proxy/{{ .VideoId }}" /> + <meta property="og:video:secure_url" content="/proxy/{{ .VideoId }}" /> + <meta property="og:video:height" content="{{ .Height }}" /> + <meta property="og:video:width" content="{{ .Width }}" /> + <meta property="og:video:duration" content="{{ .Duration }}"> + <meta property="og:video:type" content="video/mp4" /> +</head><body></body></html>