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