From 54fa59cbb0ae90a54136522c944410e2d18c234b Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 24 Aug 2023 14:58:50 -0700 Subject: Add method to construct WebVTT files Similar to JSON.Build --- spec/helpers/vtt/builder_spec.cr | 64 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 spec/helpers/vtt/builder_spec.cr (limited to 'spec/helpers/vtt/builder_spec.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 -- cgit v1.2.3 From d371eb50f27b9d29bc68ec883d8bee54894c79a4 Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 24 Aug 2023 15:42:42 -0700 Subject: 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(-) (limited to 'spec/helpers/vtt/builder_spec.cr') 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 = "#{md["text"]}" 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 -- cgit v1.2.3 From 0ad2eff2a46c28a877de1960a2dc5c15c0f94444 Mon Sep 17 00:00:00 2001 From: syeopite Date: Tue, 30 Jan 2024 15:25:45 -0800 Subject: WebVTT::Builder: Add logic to escape special chars --- spec/helpers/vtt/builder_spec.cr | 65 +++++++++++++++++++++++++++------------- src/invidious/helpers/webvtt.cr | 16 +++++++++- 2 files changed, 59 insertions(+), 22 deletions(-) (limited to 'spec/helpers/vtt/builder_spec.cr') diff --git a/spec/helpers/vtt/builder_spec.cr b/spec/helpers/vtt/builder_spec.cr index 7b543ddc..dc1f4613 100644 --- a/spec/helpers/vtt/builder_spec.cr +++ b/spec/helpers/vtt/builder_spec.cr @@ -1,34 +1,27 @@ 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", - }, -] +MockLines = ["Line 1", "Line 2"] +MockLinesWithEscapableCharacter = ["", "&Line 2>", '\u200E' + "Line\u200F 3", "\u00A0Line 4"] Spectator.describe "WebVTT::Builder" do it "correctly builds a vtt file" do result = WebVTT.build do |vtt| - MockLines.each do |line| - vtt.cue(line["start_time"], line["end_time"], line["text"]) + 2.times do |i| + vtt.cue( + Time::Span.new(seconds: i), + Time::Span.new(seconds: i + 1), + MockLines[i] + ) end end expect(result).to eq([ "WEBVTT", "", - "00:00:01.000 --> 00:00:02.000", + "00:00:00.000 --> 00:00:01.000", "Line 1", "", - "00:00:02.000 --> 00:00:03.000", + "00:00:01.000 --> 00:00:02.000", "Line 2", "", "", @@ -42,8 +35,12 @@ Spectator.describe "WebVTT::Builder" do } result = WebVTT.build(setting_fields) do |vtt| - MockLines.each do |line| - vtt.cue(line["start_time"], line["end_time"], line["text"]) + 2.times do |i| + vtt.cue( + Time::Span.new(seconds: i), + Time::Span.new(seconds: i + 1), + MockLines[i] + ) end end @@ -52,13 +49,39 @@ Spectator.describe "WebVTT::Builder" do "Kind: captions", "Language: en", "", - "00:00:01.000 --> 00:00:02.000", + "00:00:00.000 --> 00:00:01.000", "Line 1", "", - "00:00:02.000 --> 00:00:03.000", + "00:00:01.000 --> 00:00:02.000", "Line 2", "", "", ].join('\n')) end + + it "properly escapes characters" do + result = WebVTT.build do |vtt| + 4.times do |i| + vtt.cue(Time::Span.new(seconds: i), Time::Span.new(seconds: i + 1), MockLinesWithEscapableCharacter[i]) + end + end + + expect(result).to eq([ + "WEBVTT", + "", + "00:00:00.000 --> 00:00:01.000", + "<Line 1>", + "", + "00:00:01.000 --> 00:00:02.000", + "&Line 2>", + "", + "00:00:02.000 --> 00:00:03.000", + "‎Line‏ 3", + "", + "00:00:03.000 --> 00:00:04.000", + " Line 4", + "", + "", + ].join('\n')) + end end diff --git a/src/invidious/helpers/webvtt.cr b/src/invidious/helpers/webvtt.cr index 56f761ed..260d250f 100644 --- a/src/invidious/helpers/webvtt.cr +++ b/src/invidious/helpers/webvtt.cr @@ -4,13 +4,23 @@ module WebVTT # A WebVTT builder generates WebVTT files private class Builder + # See https://developer.mozilla.org/en-US/docs/Web/API/WebVTT_API#cue_payload + private ESCAPE_SUBSTITUTIONS = { + '&' => "&", + '<' => "<", + '>' => ">", + '\u200E' => "‎", + '\u200F' => "‏", + '\u00A0' => " ", + } + def initialize(@io : IO) end # 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 << self.escape(text) @io << "\n\n" end @@ -29,6 +39,10 @@ module WebVTT @io << '.' << timestamp.milliseconds.to_s.rjust(3, '0') end + private def escape(text : String) : String + return text.gsub(ESCAPE_SUBSTITUTIONS) + end + def document(setting_fields : Hash(String, String)? = nil, &) @io << "WEBVTT\n" -- cgit v1.2.3