all repos — gopipe @ 6eff4ae6fb13714b5c8a946da03292c0f530ac4e

Embed YouTube videos on Telegram, Discord and more!

add basic subtitles
Marco Andronaco andronacomarco@gmail.com
Wed, 16 Oct 2024 15:24:38 +0200
commit

6eff4ae6fb13714b5c8a946da03292c0f530ac4e

parent

8223ef17cd00edc14c764e8f9874a05ee2073d0b

M src/app/handlers.gosrc/app/handlers.go

@@ -105,11 +105,12 @@

data := map[string]interface{}{ "VideoID": videoID, "VideoURL": videoURL, - "Uploader": video.Author, + "Author": video.Author, "Title": video.Title, "Description": video.Description, "Thumbnail": thumbnail, "Duration": video.Duration, + "Captions": getCaptions(*video), "Debug": g.Debug, "Heading": template.HTML(heading), }
M src/app/main.gosrc/app/main.go

@@ -56,6 +56,7 @@ r.HandleFunc("GET /{videoID}/{formatID}", videoHandler)

r.HandleFunc("GET /proxy/{videoID}", proxyHandler) r.HandleFunc("GET /proxy/{videoID}/{formatID}", proxyHandler) + r.HandleFunc("GET /sub/{videoID}/{language}", subHandler) // r.HandleFunc("GET /robots.txt", robotsHandler)
M src/app/proxy.gosrc/app/proxy.go

@@ -7,9 +7,11 @@ "io"

"log" "net/http" "strconv" + "strings" "time" g "github.com/birabittoh/gopipe/src/globals" + "github.com/birabittoh/gopipe/src/subs" ) func proxyHandler(w http.ResponseWriter, r *http.Request) {

@@ -75,3 +77,41 @@ http.Error(w, err500, http.StatusInternalServerError)

return } } + +func subHandler(w http.ResponseWriter, r *http.Request) { + videoID := r.PathValue("videoID") + + video, err := g.KS.Get(videoID) + if err != nil || video == nil { + http.Error(w, err404, http.StatusNotFound) + return + } + + captions := getCaptions(*video) + caption, ok := captions[strings.TrimSuffix(r.PathValue("language"), ".vtt")] + if !ok { + http.Error(w, err404, http.StatusNotFound) + return + } + + res, err := g.C.Get(caption.URL) + if err != nil { + http.Error(w, err500, http.StatusInternalServerError) + return + } + defer res.Body.Close() + + w.Header().Set("Content-Type", "text/vtt") + + content, err := io.ReadAll(res.Body) + if err != nil { + http.Error(w, err500, http.StatusInternalServerError) + return + } + + err = subs.Parse(content, w) + if err != nil { + http.Error(w, err500, http.StatusInternalServerError) + return + } +}
M src/app/utils.gosrc/app/utils.go

@@ -6,6 +6,12 @@ "strconv"

"strings" ) +type Captions struct { + VideoID string + Language string + URL string +} + func getEnvDefault(key string, def string) string { res := os.Getenv(key) if res == "" {
M src/app/video.gosrc/app/video.go

@@ -51,6 +51,19 @@

return nil } +func getCaptions(video youtube.Video) map[string]Captions { + c := make(map[string]Captions) + for _, caption := range video.CaptionTracks { + c[caption.LanguageCode] = Captions{ + VideoID: video.ID, + Language: caption.LanguageCode, + URL: caption.BaseURL, + } + } + + return c +} + func formatsSelectFn(f youtube.Format) bool { return f.AudioChannels > 1 && f.ContentLength < maxContentLength && strings.HasPrefix(f.MimeType, "video/mp4") }
A src/subs/subs.go

@@ -0,0 +1,57 @@

+package subs + +import ( + "encoding/xml" + "fmt" + "log" + "net/http" +) + +type TimedText struct { + XMLName xml.Name `xml:"timedtext"` + Body Body `xml:"body"` +} + +type Body struct { + Paragraphs []Paragraph `xml:"p"` +} + +type Paragraph struct { + Text string `xml:",chardata"` + Start int `xml:"t,attr"` // Start time in milliseconds + Length int `xml:"d,attr"` // Duration in milliseconds +} + +// Convert milliseconds to WebVTT timestamp format: HH:MM:SS.mmm +func millisecondsToTimestamp(ms int) string { + seconds := ms / 1000 + milliseconds := ms % 1000 + return fmt.Sprintf("%02d:%02d:%02d.%03d", seconds/3600, (seconds%3600)/60, seconds%60, milliseconds) +} + +func writeString(s string, w http.ResponseWriter) { + w.Write([]byte(s)) +} + +func Parse(inputData []byte, w http.ResponseWriter) error { + var timedText TimedText + err := xml.Unmarshal(inputData, &timedText) + if err != nil { + log.Println("Error unmarshalling XML:", err) + return err + } + + // Write the WebVTT header + writeString("WEBVTT\n\n", w) + + // Loop through the paragraphs and write them to the WebVTT file + for i, p := range timedText.Body.Paragraphs { + startTime := millisecondsToTimestamp(p.Start) + endTime := millisecondsToTimestamp(p.Start + p.Length) + + s := fmt.Sprintf("%d\n%s --> %s\n%s\n\n", i+1, startTime, endTime, p.Text) + writeString(s, w) + } + + return nil +}
M templates/video.tmpltemplates/video.tmpl

@@ -9,12 +9,12 @@ {{ end }}

<meta property="og:url" content="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:site" content="{{ .Author }}" /> + <meta property="twitter:creator" content="{{ .Author }}" /> <meta property="twitter:title" content="{{ .Title }}" /> <meta property="og:title" content="{{ .Title }}" /> <meta property="og:description" content="{{ .Description }}" /> - <meta property="og:site_name" content="GoTube ({{ .Uploader }})" /> + <meta property="og:site_name" content="GoTube ({{ .Author }})" /> <meta property="twitter:image" content="{{ .Thumbnail }}" /> <meta property="twitter:player:stream:content_type" content="video/mp4" /> {{ if .VideoURL }}

@@ -29,9 +29,12 @@

{{define "content" -}} <video style="width: 100%" autoplay controls> <source src="{{ .VideoURL }}" type="video/mp4" /> + {{ range .Captions }} + <track kind="subtitles" label="{{ .Language }}" src="/sub/{{ .VideoID }}/{{ .Language }}.vtt" srclang="{{ .Language }}" /> + {{ end }} </video> <h2>{{ .Title }}</h2> - <h3>&gt; {{ .Uploader }}</h3> + <h3>&gt; {{ .Author }}</h3> <pre style="white-space: pre-wrap">{{ .Description }}</pre> <a href="https://www.youtube.com/watch?v={{ .VideoID }}">Watch on YouTube</a> <br />