From 54fa59cbb0ae90a54136522c944410e2d18c234b Mon Sep 17 00:00:00 2001
From: syeopite <syeopite@syeopite.dev>
Date: Thu, 24 Aug 2023 14:58:50 -0700
Subject: [PATCH 1/7] Add method to construct WebVTT files

Similar to JSON.Build
---
 spec/helpers/vtt/builder_spec.cr | 64 ++++++++++++++++++++++++++++++
 src/invidious/helpers/webvtt.cr  | 67 ++++++++++++++++++++++++++++++++
 2 files changed, 131 insertions(+)
 create mode 100644 spec/helpers/vtt/builder_spec.cr
 create mode 100644 src/invidious/helpers/webvtt.cr

diff --git a/spec/helpers/vtt/builder_spec.cr b/spec/helpers/vtt/builder_spec.cr
new file mode 100644
index 00000000..69303bab
--- /dev/null
+++ b/spec/helpers/vtt/builder_spec.cr
@@ -0,0 +1,64 @@
+require "../../spec_helper.cr"
+
+MockLines = [
+  {
+    "start_time": Time::Span.new(seconds: 1),
+    "end_time":   Time::Span.new(seconds: 2),
+    "text":       "Line 1",
+  },
+
+  {
+    "start_time": Time::Span.new(seconds: 2),
+    "end_time":   Time::Span.new(seconds: 3),
+    "text":       "Line 2",
+  },
+]
+
+Spectator.describe "WebVTT::Builder" do
+  it "correctly builds a vtt file" do
+    result = WebVTT.build do |vtt|
+      MockLines.each do |line|
+        vtt.line(line["start_time"], line["end_time"], line["text"])
+      end
+    end
+
+    expect(result).to eq([
+      "WEBVTT",
+      "",
+      "00:00:01.000 --> 00:00:02.000",
+      "Line 1",
+      "",
+      "00:00:02.000 --> 00:00:03.000",
+      "Line 2",
+      "",
+      "",
+    ].join('\n'))
+  end
+
+  it "correctly builds a vtt file with setting fields" do
+    setting_fields = {
+      "Kind"     => "captions",
+      "Language" => "en",
+    }
+
+    result = WebVTT.build(setting_fields) do |vtt|
+      MockLines.each do |line|
+        vtt.line(line["start_time"], line["end_time"], line["text"])
+      end
+    end
+
+    expect(result).to eq([
+      "WEBVTT",
+      "Kind: captions",
+      "Language: en",
+      "",
+      "00:00:01.000 --> 00:00:02.000",
+      "Line 1",
+      "",
+      "00:00:02.000 --> 00:00:03.000",
+      "Line 2",
+      "",
+      "",
+    ].join('\n'))
+  end
+end
diff --git a/src/invidious/helpers/webvtt.cr b/src/invidious/helpers/webvtt.cr
new file mode 100644
index 00000000..7d9d5f1f
--- /dev/null
+++ b/src/invidious/helpers/webvtt.cr
@@ -0,0 +1,67 @@
+# Namespace for logic relating to generating WebVTT files
+#
+# Probably not compliant to WebVTT's specs but it is enough for Invidious.
+module WebVTT
+  # A WebVTT builder generates WebVTT files
+  private class Builder
+    def initialize(@io : IO)
+    end
+
+    # Writes an vtt line with the specified time stamp and contents
+    def line(start_time : Time::Span, end_time : Time::Span, text : String)
+      timestamp(start_time, end_time)
+      @io << text
+      @io << "\n\n"
+    end
+
+    private def timestamp(start_time : Time::Span, end_time : Time::Span)
+      add_timestamp_component(start_time)
+      @io << " --> "
+      add_timestamp_component(end_time)
+
+      @io << '\n'
+    end
+
+    private def add_timestamp_component(timestamp : Time::Span)
+      @io << timestamp.hours.to_s.rjust(2, '0')
+      @io << ':' << timestamp.minutes.to_s.rjust(2, '0')
+      @io << ':' << timestamp.seconds.to_s.rjust(2, '0')
+      @io << '.' << timestamp.milliseconds.to_s.rjust(3, '0')
+    end
+
+    def document(setting_fields : Hash(String, String)? = nil, &)
+      @io << "WEBVTT\n"
+
+      if setting_fields
+        setting_fields.each do |name, value|
+          @io << "#{name}: #{value}\n"
+        end
+      end
+
+      @io << '\n'
+
+      yield
+    end
+  end
+
+  # Returns the resulting `String` of writing WebVTT to the yielded WebVTT::Builder
+  #
+  # ```
+  # string = WebVTT.build do |io|
+  #   vtt.line(Time::Span.new(seconds: 1), Time::Span.new(seconds: 2), "Line 1")
+  #   vtt.line(Time::Span.new(seconds: 2), Time::Span.new(seconds: 3), "Line 2")
+  # end
+  #
+  # string # => "WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nLine 1\n\n00:00:02.000 --> 00:00:03.000\nLine 2\n\n"
+  # ```
+  #
+  # Accepts an optional settings fields hash to add settings attribute to the resulting vtt file.
+  def self.build(setting_fields : Hash(String, String)? = nil, &)
+    String.build do |str|
+      builder = Builder.new(str)
+      builder.document(setting_fields) do
+        yield builder
+      end
+    end
+  end
+end

From 0cb7d0b44137c2cee9b6352969a28dac4e3576c5 Mon Sep 17 00:00:00 2001
From: syeopite <syeopite@syeopite.dev>
Date: Thu, 24 Aug 2023 15:10:50 -0700
Subject: [PATCH 2/7] Refactor Invidious's VTT logic to use WebVtt.build

---
 src/invidious/routes/api/v1/videos.cr | 39 +++++++------------------
 src/invidious/videos/caption.cr       | 41 ++++++++-------------------
 src/invidious/videos/transcript.cr    | 40 +++++---------------------
 3 files changed, 29 insertions(+), 91 deletions(-)

diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr
index 25e766d2..5c50a804 100644
--- a/src/invidious/routes/api/v1/videos.cr
+++ b/src/invidious/routes/api/v1/videos.cr
@@ -101,20 +101,17 @@ module Invidious::Routes::API::V1::Videos
       if caption.name.includes? "auto-generated"
         caption_xml = YT_POOL.client &.get(url).body
 
+        settings_field = {
+          "Kind"     => "captions",
+          "Language" => "#{tlang || caption.language_code}",
+        }
+
         if caption_xml.starts_with?("<?xml")
           webvtt = caption.timedtext_to_vtt(caption_xml, tlang)
         else
           caption_xml = XML.parse(caption_xml)
 
-          webvtt = String.build do |str|
-            str << <<-END_VTT
-            WEBVTT
-            Kind: captions
-            Language: #{tlang || caption.language_code}
-
-
-            END_VTT
-
+          webvtt = WebVTT.build(settings_field) do |webvtt|
             caption_nodes = caption_xml.xpath_nodes("//transcript/text")
             caption_nodes.each_with_index do |node, i|
               start_time = node["start"].to_f.seconds
@@ -127,9 +124,6 @@ module Invidious::Routes::API::V1::Videos
                 end_time = start_time + duration
               end
 
-              start_time = "#{start_time.hours.to_s.rjust(2, '0')}:#{start_time.minutes.to_s.rjust(2, '0')}:#{start_time.seconds.to_s.rjust(2, '0')}.#{start_time.milliseconds.to_s.rjust(3, '0')}"
-              end_time = "#{end_time.hours.to_s.rjust(2, '0')}:#{end_time.minutes.to_s.rjust(2, '0')}:#{end_time.seconds.to_s.rjust(2, '0')}.#{end_time.milliseconds.to_s.rjust(3, '0')}"
-
               text = HTML.unescape(node.content)
               text = text.gsub(/<font color="#[a-fA-F0-9]{6}">/, "")
               text = text.gsub(/<\/font>/, "")
@@ -137,12 +131,7 @@ module Invidious::Routes::API::V1::Videos
                 text = "<v #{md["name"]}>#{md["text"]}</v>"
               end
 
-              str << <<-END_CUE
-              #{start_time} --> #{end_time}
-              #{text}
-
-
-              END_CUE
+              webvtt.line(start_time, end_time, text)
             end
           end
         end
@@ -215,11 +204,7 @@ module Invidious::Routes::API::V1::Videos
       storyboard = storyboard[0]
     end
 
-    String.build do |str|
-      str << <<-END_VTT
-      WEBVTT
-      END_VTT
-
+    WebVTT.build do |vtt|
       start_time = 0.milliseconds
       end_time = storyboard[:interval].milliseconds
 
@@ -231,12 +216,8 @@ module Invidious::Routes::API::V1::Videos
 
         storyboard[:storyboard_height].times do |j|
           storyboard[:storyboard_width].times do |k|
-            str << <<-END_CUE
-            #{start_time}.000 --> #{end_time}.000
-            #{url}#xywh=#{storyboard[:width] * k},#{storyboard[:height] * j},#{storyboard[:width] - 2},#{storyboard[:height]}
-
-
-            END_CUE
+            current_cue_url = "#{url}#xywh=#{storyboard[:width] * k},#{storyboard[:height] * j},#{storyboard[:width] - 2},#{storyboard[:height]}"
+            vtt.line(start_time, end_time, current_cue_url)
 
             start_time += storyboard[:interval].milliseconds
             end_time += storyboard[:interval].milliseconds
diff --git a/src/invidious/videos/caption.cr b/src/invidious/videos/caption.cr
index 256dfcc0..dc58f9a0 100644
--- a/src/invidious/videos/caption.cr
+++ b/src/invidious/videos/caption.cr
@@ -52,17 +52,13 @@ module Invidious::Videos
             break
           end
         end
-        result = String.build do |result|
-          result << <<-END_VTT
-          WEBVTT
-          Kind: captions
-          Language: #{tlang || @language_code}
 
+        settings_field = {
+          "Kind"     => "captions",
+          "Language" => "#{tlang || @language_code}",
+        }
 
-          END_VTT
-
-          result << "\n\n"
-
+        result = WebVTT.build(settings_field) do |vtt|
           cues.each_with_index do |node, i|
             start_time = node["t"].to_f.milliseconds
 
@@ -76,29 +72,16 @@ module Invidious::Videos
               end_time = start_time + duration
             end
 
-            # start_time
-            result << start_time.hours.to_s.rjust(2, '0')
-            result << ':' << start_time.minutes.to_s.rjust(2, '0')
-            result << ':' << start_time.seconds.to_s.rjust(2, '0')
-            result << '.' << start_time.milliseconds.to_s.rjust(3, '0')
-
-            result << " --> "
-
-            # end_time
-            result << end_time.hours.to_s.rjust(2, '0')
-            result << ':' << end_time.minutes.to_s.rjust(2, '0')
-            result << ':' << end_time.seconds.to_s.rjust(2, '0')
-            result << '.' << end_time.milliseconds.to_s.rjust(3, '0')
-
-            result << "\n"
-
-            node.children.each do |s|
-              result << s.content
+            text = String.build do |io|
+              node.children.each do |s|
+                io << s.content
+              end
             end
-            result << "\n"
-            result << "\n"
+
+            vtt.line(start_time, end_time, text)
           end
         end
+
         return result
       end
     end
diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr
index f3360a52..cd97cfde 100644
--- a/src/invidious/videos/transcript.cr
+++ b/src/invidious/videos/transcript.cr
@@ -34,41 +34,15 @@ module Invidious::Videos
       # Convert into array of TranscriptLine
       lines = self.parse(initial_data)
 
+      settings_field = {
+        "Kind" => "captions",
+        "Language" => target_language
+      }
+
       # Taken from Invidious::Videos::Captions::Metadata.timedtext_to_vtt()
-      vtt = String.build do |vtt|
-        vtt << <<-END_VTT
-        WEBVTT
-        Kind: captions
-        Language: #{target_language}
-
-
-        END_VTT
-
-        vtt << "\n\n"
-
+      vtt = WebVTT.build(settings_field) do |vtt|
         lines.each do |line|
-          start_time = line.start_ms
-          end_time = line.end_ms
-
-          # start_time
-          vtt << start_time.hours.to_s.rjust(2, '0')
-          vtt << ':' << start_time.minutes.to_s.rjust(2, '0')
-          vtt << ':' << start_time.seconds.to_s.rjust(2, '0')
-          vtt << '.' << start_time.milliseconds.to_s.rjust(3, '0')
-
-          vtt << " --> "
-
-          # end_time
-          vtt << end_time.hours.to_s.rjust(2, '0')
-          vtt << ':' << end_time.minutes.to_s.rjust(2, '0')
-          vtt << ':' << end_time.seconds.to_s.rjust(2, '0')
-          vtt << '.' << end_time.milliseconds.to_s.rjust(3, '0')
-
-          vtt << "\n"
-          vtt << line.line
-
-          vtt << "\n"
-          vtt << "\n"
+          vtt.line(line.start_ms, line.end_ms, line.line)
         end
       end
 

From d371eb50f27b9d29bc68ec883d8bee54894c79a4 Mon Sep 17 00:00:00 2001
From: syeopite <syeopite@syeopite.dev>
Date: Thu, 24 Aug 2023 15:42:42 -0700
Subject: [PATCH 3/7] WebVTT::Builder: rename #line to #cue

---
 spec/helpers/vtt/builder_spec.cr      | 4 ++--
 src/invidious/helpers/webvtt.cr       | 8 ++++----
 src/invidious/routes/api/v1/videos.cr | 4 ++--
 src/invidious/videos/caption.cr       | 2 +-
 src/invidious/videos/transcript.cr    | 2 +-
 5 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/spec/helpers/vtt/builder_spec.cr b/spec/helpers/vtt/builder_spec.cr
index 69303bab..7b543ddc 100644
--- a/spec/helpers/vtt/builder_spec.cr
+++ b/spec/helpers/vtt/builder_spec.cr
@@ -18,7 +18,7 @@ Spectator.describe "WebVTT::Builder" do
   it "correctly builds a vtt file" do
     result = WebVTT.build do |vtt|
       MockLines.each do |line|
-        vtt.line(line["start_time"], line["end_time"], line["text"])
+        vtt.cue(line["start_time"], line["end_time"], line["text"])
       end
     end
 
@@ -43,7 +43,7 @@ Spectator.describe "WebVTT::Builder" do
 
     result = WebVTT.build(setting_fields) do |vtt|
       MockLines.each do |line|
-        vtt.line(line["start_time"], line["end_time"], line["text"])
+        vtt.cue(line["start_time"], line["end_time"], line["text"])
       end
     end
 
diff --git a/src/invidious/helpers/webvtt.cr b/src/invidious/helpers/webvtt.cr
index 7d9d5f1f..c50d7fa2 100644
--- a/src/invidious/helpers/webvtt.cr
+++ b/src/invidious/helpers/webvtt.cr
@@ -7,8 +7,8 @@ module WebVTT
     def initialize(@io : IO)
     end
 
-    # Writes an vtt line with the specified time stamp and contents
-    def line(start_time : Time::Span, end_time : Time::Span, text : String)
+    # Writes an vtt cue with the specified time stamp and contents
+    def cue(start_time : Time::Span, end_time : Time::Span, text : String)
       timestamp(start_time, end_time)
       @io << text
       @io << "\n\n"
@@ -48,8 +48,8 @@ module WebVTT
   #
   # ```
   # string = WebVTT.build do |io|
-  #   vtt.line(Time::Span.new(seconds: 1), Time::Span.new(seconds: 2), "Line 1")
-  #   vtt.line(Time::Span.new(seconds: 2), Time::Span.new(seconds: 3), "Line 2")
+  #   vtt.cue(Time::Span.new(seconds: 1), Time::Span.new(seconds: 2), "Line 1")
+  #   vtt.cue(Time::Span.new(seconds: 2), Time::Span.new(seconds: 3), "Line 2")
   # end
   #
   # string # => "WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nLine 1\n\n00:00:02.000 --> 00:00:03.000\nLine 2\n\n"
diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr
index 5c50a804..449c9f9b 100644
--- a/src/invidious/routes/api/v1/videos.cr
+++ b/src/invidious/routes/api/v1/videos.cr
@@ -131,7 +131,7 @@ module Invidious::Routes::API::V1::Videos
                 text = "<v #{md["name"]}>#{md["text"]}</v>"
               end
 
-              webvtt.line(start_time, end_time, text)
+              webvtt.cue(start_time, end_time, text)
             end
           end
         end
@@ -217,7 +217,7 @@ module Invidious::Routes::API::V1::Videos
         storyboard[:storyboard_height].times do |j|
           storyboard[:storyboard_width].times do |k|
             current_cue_url = "#{url}#xywh=#{storyboard[:width] * k},#{storyboard[:height] * j},#{storyboard[:width] - 2},#{storyboard[:height]}"
-            vtt.line(start_time, end_time, current_cue_url)
+            vtt.cue(start_time, end_time, current_cue_url)
 
             start_time += storyboard[:interval].milliseconds
             end_time += storyboard[:interval].milliseconds
diff --git a/src/invidious/videos/caption.cr b/src/invidious/videos/caption.cr
index dc58f9a0..484e61d2 100644
--- a/src/invidious/videos/caption.cr
+++ b/src/invidious/videos/caption.cr
@@ -78,7 +78,7 @@ module Invidious::Videos
               end
             end
 
-            vtt.line(start_time, end_time, text)
+            vtt.cue(start_time, end_time, text)
           end
         end
 
diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr
index cd97cfde..055d96fb 100644
--- a/src/invidious/videos/transcript.cr
+++ b/src/invidious/videos/transcript.cr
@@ -42,7 +42,7 @@ module Invidious::Videos
       # Taken from Invidious::Videos::Captions::Metadata.timedtext_to_vtt()
       vtt = WebVTT.build(settings_field) do |vtt|
         lines.each do |line|
-          vtt.line(line.start_ms, line.end_ms, line.line)
+          vtt.cue(line.start_ms, line.end_ms, line.line)
         end
       end
 

From 4e97d8ad0942bd64a23ed4a2ba89e48a97c520aa Mon Sep 17 00:00:00 2001
From: syeopite <syeopite@syeopite.dev>
Date: Thu, 24 Aug 2023 16:27:06 -0700
Subject: [PATCH 4/7] Update documentation for `WebVTT.build`

---
 src/invidious/helpers/webvtt.cr | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/invidious/helpers/webvtt.cr b/src/invidious/helpers/webvtt.cr
index c50d7fa2..52138854 100644
--- a/src/invidious/helpers/webvtt.cr
+++ b/src/invidious/helpers/webvtt.cr
@@ -44,10 +44,10 @@ module WebVTT
     end
   end
 
-  # Returns the resulting `String` of writing WebVTT to the yielded WebVTT::Builder
+  # Returns the resulting `String` of writing WebVTT to the yielded `WebVTT::Builder`
   #
   # ```
-  # string = WebVTT.build do |io|
+  # string = WebVTT.build do |vtt|
   #   vtt.cue(Time::Span.new(seconds: 1), Time::Span.new(seconds: 2), "Line 1")
   #   vtt.cue(Time::Span.new(seconds: 2), Time::Span.new(seconds: 3), "Line 2")
   # end

From e9d59a6dfd14fd115f3bfc59ca6f33182a631575 Mon Sep 17 00:00:00 2001
From: syeopite <70992037+syeopite@users.noreply.github.com>
Date: Tue, 29 Aug 2023 05:59:08 +0000
Subject: [PATCH 5/7] Update src/invidious/helpers/webvtt.cr

Co-authored-by: Samantaz Fox <coding@samantaz.fr>
---
 src/invidious/helpers/webvtt.cr | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/invidious/helpers/webvtt.cr b/src/invidious/helpers/webvtt.cr
index 52138854..aace6bb8 100644
--- a/src/invidious/helpers/webvtt.cr
+++ b/src/invidious/helpers/webvtt.cr
@@ -34,7 +34,7 @@ module WebVTT
 
       if setting_fields
         setting_fields.each do |name, value|
-          @io << "#{name}: #{value}\n"
+          @io << name << ": " << value << '\n'
         end
       end
 

From a999438ae46739477a6ca5f8515fa70b6b492443 Mon Sep 17 00:00:00 2001
From: syeopite <syeopite@syeopite.dev>
Date: Mon, 28 Aug 2023 23:14:25 -0700
Subject: [PATCH 6/7] Consistency: rename #add_timestamp_component

Removes the add_ prefix for consistency with the other methods in
WebVTT::Builder
---
 src/invidious/helpers/webvtt.cr | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/invidious/helpers/webvtt.cr b/src/invidious/helpers/webvtt.cr
index aace6bb8..56f761ed 100644
--- a/src/invidious/helpers/webvtt.cr
+++ b/src/invidious/helpers/webvtt.cr
@@ -15,14 +15,14 @@ module WebVTT
     end
 
     private def timestamp(start_time : Time::Span, end_time : Time::Span)
-      add_timestamp_component(start_time)
+      timestamp_component(start_time)
       @io << " --> "
-      add_timestamp_component(end_time)
+      timestamp_component(end_time)
 
       @io << '\n'
     end
 
-    private def add_timestamp_component(timestamp : Time::Span)
+    private def timestamp_component(timestamp : Time::Span)
       @io << timestamp.hours.to_s.rjust(2, '0')
       @io << ':' << timestamp.minutes.to_s.rjust(2, '0')
       @io << ':' << timestamp.seconds.to_s.rjust(2, '0')

From be2feba17c2f3b9d8e043825beff57568df46f2e Mon Sep 17 00:00:00 2001
From: syeopite <syeopite@syeopite.dev>
Date: Sat, 23 Sep 2023 09:57:26 -0400
Subject: [PATCH 7/7] Lint

---
 src/invidious/videos/transcript.cr | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr
index 055d96fb..dac00eea 100644
--- a/src/invidious/videos/transcript.cr
+++ b/src/invidious/videos/transcript.cr
@@ -35,8 +35,8 @@ module Invidious::Videos
       lines = self.parse(initial_data)
 
       settings_field = {
-        "Kind" => "captions",
-        "Language" => target_language
+        "Kind"     => "captions",
+        "Language" => target_language,
       }
 
       # Taken from Invidious::Videos::Captions::Metadata.timedtext_to_vtt()