add basic subtitles
Marco Andronaco andronacomarco@gmail.com
Wed, 16 Oct 2024 15:24:38 +0200
7 files changed,
126 insertions(+),
5 deletions(-)
M
src/app/handlers.go
→
src/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.go
→
src/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.go
→
src/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.go
→
src/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.go
→
src/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.tmpl
→
templates/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>> {{ .Uploader }}</h3> + <h3>> {{ .Author }}</h3> <pre style="white-space: pre-wrap">{{ .Description }}</pre> <a href="https://www.youtube.com/watch?v={{ .VideoID }}">Watch on YouTube</a> <br />