From 6878822c4d621bc2a2ba65c117efc65246e9a1ca Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 8 Oct 2023 12:58:04 +0200 Subject: [PATCH 1/8] Storyboards: Move parser to its own file --- src/invidious/videos.cr | 61 +------------------------- src/invidious/videos/storyboard.cr | 69 ++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 59 deletions(-) create mode 100644 src/invidious/videos/storyboard.cr diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 6d0cf9ba0..733219094 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -177,65 +177,8 @@ struct Video # Misc. methods def storyboards - storyboards = info.dig?("storyboards", "playerStoryboardSpecRenderer", "spec") - .try &.as_s.split("|") - - if !storyboards - if storyboard = info.dig?("storyboards", "playerLiveStoryboardSpecRenderer", "spec").try &.as_s - return [{ - url: storyboard.split("#")[0], - width: 106, - height: 60, - count: -1, - interval: 5000, - storyboard_width: 3, - storyboard_height: 3, - storyboard_count: -1, - }] - end - end - - items = [] of NamedTuple( - url: String, - width: Int32, - height: Int32, - count: Int32, - interval: Int32, - storyboard_width: Int32, - storyboard_height: Int32, - storyboard_count: Int32) - - return items if !storyboards - - url = URI.parse(storyboards.shift) - params = HTTP::Params.parse(url.query || "") - - storyboards.each_with_index do |sb, i| - width, height, count, storyboard_width, storyboard_height, interval, _, sigh = sb.split("#") - params["sigh"] = sigh - url.query = params.to_s - - width = width.to_i - height = height.to_i - count = count.to_i - interval = interval.to_i - storyboard_width = storyboard_width.to_i - storyboard_height = storyboard_height.to_i - storyboard_count = (count / (storyboard_width * storyboard_height)).ceil.to_i - - items << { - url: url.to_s.sub("$L", i).sub("$N", "M$M"), - width: width, - height: height, - count: count, - interval: interval, - storyboard_width: storyboard_width, - storyboard_height: storyboard_height, - storyboard_count: storyboard_count, - } - end - - items + container = info.dig?("storyboards") || JSON::Any.new("{}") + return IV::Videos::Storyboard.from_yt_json(container) end def paid diff --git a/src/invidious/videos/storyboard.cr b/src/invidious/videos/storyboard.cr new file mode 100644 index 000000000..b4302d882 --- /dev/null +++ b/src/invidious/videos/storyboard.cr @@ -0,0 +1,69 @@ +require "uri" +require "http/params" + +module Invidious::Videos + struct Storyboard + # Parse the JSON structure from Youtube + def self.from_yt_json(container : JSON::Any) + storyboards = container.dig?("playerStoryboardSpecRenderer", "spec") + .try &.as_s.split("|") + + if !storyboards + if storyboard = container.dig?("playerLiveStoryboardSpecRenderer", "spec").try &.as_s + return [{ + url: storyboard.split("#")[0], + width: 106, + height: 60, + count: -1, + interval: 5000, + storyboard_width: 3, + storyboard_height: 3, + storyboard_count: -1, + }] + end + end + + items = [] of NamedTuple( + url: String, + width: Int32, + height: Int32, + count: Int32, + interval: Int32, + storyboard_width: Int32, + storyboard_height: Int32, + storyboard_count: Int32) + + return items if !storyboards + + url = URI.parse(storyboards.shift) + params = HTTP::Params.parse(url.query || "") + + storyboards.each_with_index do |sb, i| + width, height, count, storyboard_width, storyboard_height, interval, _, sigh = sb.split("#") + params["sigh"] = sigh + url.query = params.to_s + + width = width.to_i + height = height.to_i + count = count.to_i + interval = interval.to_i + storyboard_width = storyboard_width.to_i + storyboard_height = storyboard_height.to_i + storyboard_count = (count / (storyboard_width * storyboard_height)).ceil.to_i + + items << { + url: url.to_s.sub("$L", i).sub("$N", "M$M"), + width: width, + height: height, + count: count, + interval: interval, + storyboard_width: storyboard_width, + storyboard_height: storyboard_height, + storyboard_count: storyboard_count, + } + end + + items + end + end +end From 8327862697774cd8076335fe2002875dd8c5a84a Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 8 Oct 2023 20:09:38 +0200 Subject: [PATCH 2/8] Storyboards: Use replace the NamedTuple by a struct --- src/invidious/jsonify/api_v1/video_json.cr | 18 +++---- src/invidious/routes/api/v1/videos.cr | 19 +++---- src/invidious/videos/storyboard.cr | 61 ++++++++++++---------- 3 files changed, 53 insertions(+), 45 deletions(-) diff --git a/src/invidious/jsonify/api_v1/video_json.cr b/src/invidious/jsonify/api_v1/video_json.cr index 597148281..44a34b18d 100644 --- a/src/invidious/jsonify/api_v1/video_json.cr +++ b/src/invidious/jsonify/api_v1/video_json.cr @@ -273,15 +273,15 @@ module Invidious::JSONify::APIv1 json.array do storyboards.each do |storyboard| json.object do - json.field "url", "/api/v1/storyboards/#{id}?width=#{storyboard[:width]}&height=#{storyboard[:height]}" - json.field "templateUrl", storyboard[:url] - json.field "width", storyboard[:width] - json.field "height", storyboard[:height] - json.field "count", storyboard[:count] - json.field "interval", storyboard[:interval] - json.field "storyboardWidth", storyboard[:storyboard_width] - json.field "storyboardHeight", storyboard[:storyboard_height] - json.field "storyboardCount", storyboard[:storyboard_count] + json.field "url", "/api/v1/storyboards/#{id}?width=#{storyboard.width}&height=#{storyboard.height}" + json.field "templateUrl", storyboard.url + json.field "width", storyboard.width + json.field "height", storyboard.height + json.field "count", storyboard.count + json.field "interval", storyboard.interval + json.field "storyboardWidth", storyboard.storyboard_width + json.field "storyboardHeight", storyboard.storyboard_height + json.field "storyboardCount", storyboard.storyboard_count end end end diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index 42282f44d..78f91a2e5 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -205,7 +205,7 @@ module Invidious::Routes::API::V1::Videos env.response.content_type = "text/vtt" - storyboard = storyboards.select { |sb| width == "#{sb[:width]}" || height == "#{sb[:height]}" } + storyboard = storyboards.select { |sb| width == "#{sb.width}" || height == "#{sb.height}" } if storyboard.empty? haltf env, 404 @@ -215,21 +215,22 @@ module Invidious::Routes::API::V1::Videos WebVTT.build do |vtt| start_time = 0.milliseconds - end_time = storyboard[:interval].milliseconds + end_time = storyboard.interval.milliseconds - storyboard[:storyboard_count].times do |i| - url = storyboard[:url] + storyboard.storyboard_count.times do |i| + url = storyboard.url authority = /(i\d?).ytimg.com/.match!(url)[1]? + url = url.gsub("$M", i).gsub(%r(https://i\d?.ytimg.com/sb/), "") url = "#{HOST_URL}/sb/#{authority}/#{url}" - 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]}" + 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.cue(start_time, end_time, current_cue_url) - start_time += storyboard[:interval].milliseconds - end_time += storyboard[:interval].milliseconds + start_time += storyboard.interval.milliseconds + end_time += storyboard.interval.milliseconds end end end diff --git a/src/invidious/videos/storyboard.cr b/src/invidious/videos/storyboard.cr index b4302d882..797fba12b 100644 --- a/src/invidious/videos/storyboard.cr +++ b/src/invidious/videos/storyboard.cr @@ -3,6 +3,21 @@ require "http/params" module Invidious::Videos struct Storyboard + getter url : String + getter width : Int32 + getter height : Int32 + getter count : Int32 + getter interval : Int32 + getter storyboard_width : Int32 + getter storyboard_height : Int32 + getter storyboard_count : Int32 + + def initialize( + *, @url, @width, @height, @count, @interval, + @storyboard_width, @storyboard_height, @storyboard_count + ) + end + # Parse the JSON structure from Youtube def self.from_yt_json(container : JSON::Any) storyboards = container.dig?("playerStoryboardSpecRenderer", "spec") @@ -10,28 +25,20 @@ module Invidious::Videos if !storyboards if storyboard = container.dig?("playerLiveStoryboardSpecRenderer", "spec").try &.as_s - return [{ - url: storyboard.split("#")[0], - width: 106, - height: 60, - count: -1, - interval: 5000, - storyboard_width: 3, + return [Storyboard.new( + url: storyboard.split("#")[0], + width: 106, + height: 60, + count: -1, + interval: 5000, + storyboard_width: 3, storyboard_height: 3, - storyboard_count: -1, - }] + storyboard_count: -1, + )] end end - items = [] of NamedTuple( - url: String, - width: Int32, - height: Int32, - count: Int32, - interval: Int32, - storyboard_width: Int32, - storyboard_height: Int32, - storyboard_count: Int32) + items = [] of Storyboard return items if !storyboards @@ -51,16 +58,16 @@ module Invidious::Videos storyboard_height = storyboard_height.to_i storyboard_count = (count / (storyboard_width * storyboard_height)).ceil.to_i - items << { - url: url.to_s.sub("$L", i).sub("$N", "M$M"), - width: width, - height: height, - count: count, - interval: interval, - storyboard_width: storyboard_width, + items << Storyboard.new( + url: url.to_s.sub("$L", i).sub("$N", "M$M"), + width: width, + height: height, + count: count, + interval: interval, + storyboard_width: storyboard_width, storyboard_height: storyboard_height, - storyboard_count: storyboard_count, - } + storyboard_count: storyboard_count + ) end items From da3d58f03c9b1617f96f4caf1e348a35105dd79c Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 8 Oct 2023 20:29:41 +0200 Subject: [PATCH 3/8] Storyboards: Cleanup and document code --- src/invidious/jsonify/api_v1/video_json.cr | 20 ++-- src/invidious/routes/api/v1/videos.cr | 52 +++++----- src/invidious/videos/storyboard.cr | 114 ++++++++++++++------- 3 files changed, 113 insertions(+), 73 deletions(-) diff --git a/src/invidious/jsonify/api_v1/video_json.cr b/src/invidious/jsonify/api_v1/video_json.cr index 44a34b18d..4d12a0720 100644 --- a/src/invidious/jsonify/api_v1/video_json.cr +++ b/src/invidious/jsonify/api_v1/video_json.cr @@ -271,17 +271,17 @@ module Invidious::JSONify::APIv1 def storyboards(json, id, storyboards) json.array do - storyboards.each do |storyboard| + storyboards.each do |sb| json.object do - json.field "url", "/api/v1/storyboards/#{id}?width=#{storyboard.width}&height=#{storyboard.height}" - json.field "templateUrl", storyboard.url - json.field "width", storyboard.width - json.field "height", storyboard.height - json.field "count", storyboard.count - json.field "interval", storyboard.interval - json.field "storyboardWidth", storyboard.storyboard_width - json.field "storyboardHeight", storyboard.storyboard_height - json.field "storyboardCount", storyboard.storyboard_count + json.field "url", "/api/v1/storyboards/#{id}?width=#{sb.width}&height=#{sb.height}" + json.field "templateUrl", sb.url.to_s + json.field "width", sb.width + json.field "height", sb.height + json.field "count", sb.count + json.field "interval", sb.interval + json.field "storyboardWidth", sb.columns + json.field "storyboardHeight", sb.rows + json.field "storyboardCount", sb.images_count end end end diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index 78f91a2e5..fb083934e 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -187,15 +187,14 @@ module Invidious::Routes::API::V1::Videos haltf env, 500 end - storyboards = video.storyboards - width = env.params.query["width"]? - height = env.params.query["height"]? + width = env.params.query["width"]?.try &.to_i + height = env.params.query["height"]?.try &.to_i if !width && !height response = JSON.build do |json| json.object do json.field "storyboards" do - Invidious::JSONify::APIv1.storyboards(json, id, storyboards) + Invidious::JSONify::APIv1.storyboards(json, id, video.storyboards) end end end @@ -205,32 +204,37 @@ module Invidious::Routes::API::V1::Videos env.response.content_type = "text/vtt" - storyboard = storyboards.select { |sb| width == "#{sb.width}" || height == "#{sb.height}" } + # Select a storyboard matching the user's provided width/height + storyboard = video.storyboards.select { |x| x.width == width || x.height == height } + haltf env, 404 if storyboard.empty? - if storyboard.empty? - haltf env, 404 - else - storyboard = storyboard[0] - end + # Alias variable, to make the code below esaier to read + sb = storyboard[0] - WebVTT.build do |vtt| - start_time = 0.milliseconds - end_time = storyboard.interval.milliseconds + # Some base URL segments that we'll use to craft the final URLs + work_url = sb.proxied_url.dup + template_path = sb.proxied_url.path - storyboard.storyboard_count.times do |i| - url = storyboard.url - authority = /(i\d?).ytimg.com/.match!(url)[1]? + # Initialize cue timing variables + time_delta = sb.interval.milliseconds + start_time = 0.milliseconds + end_time = time_delta - 1.milliseconds - url = url.gsub("$M", i).gsub(%r(https://i\d?.ytimg.com/sb/), "") - url = "#{HOST_URL}/sb/#{authority}/#{url}" + # Build a VTT file for VideoJS-vtt plugin + return WebVTT.build do |vtt| + sb.images_count.times do |i| + # Replace the variable component part of the path + work_url.path = template_path.sub("$M", i) - 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.cue(start_time, end_time, current_cue_url) + sb.rows.times do |j| + sb.columns.times do |k| + # The URL fragment represents the offset of the thumbnail inside the storyboard image + work_url.fragment = "xywh=#{sb.width * k},#{sb.height * j},#{sb.width - 2},#{sb.height}" - start_time += storyboard.interval.milliseconds - end_time += storyboard.interval.milliseconds + vtt.cue(start_time, end_time, work_url.to_s) + + start_time += time_delta + end_time += time_delta end end end diff --git a/src/invidious/videos/storyboard.cr b/src/invidious/videos/storyboard.cr index 797fba12b..f6df187ff 100644 --- a/src/invidious/videos/storyboard.cr +++ b/src/invidious/videos/storyboard.cr @@ -3,74 +3,110 @@ require "http/params" module Invidious::Videos struct Storyboard - getter url : String + # Template URL + getter url : URI + getter proxied_url : URI + + # Thumbnail parameters getter width : Int32 getter height : Int32 getter count : Int32 getter interval : Int32 - getter storyboard_width : Int32 - getter storyboard_height : Int32 - getter storyboard_count : Int32 + + # Image (storyboard) parameters + getter rows : Int32 + getter columns : Int32 + getter images_count : Int32 def initialize( *, @url, @width, @height, @count, @interval, - @storyboard_width, @storyboard_height, @storyboard_count + @rows, @columns, @images_count ) + authority = /(i\d?).ytimg.com/.match(@url.host.not_nil!).not_nil![1]? + + @proxied_url = URI.parse(HOST_URL) + @proxied_url.path = "/sb/#{authority}#{@url.path}" + @proxied_url.query = @url.query end # Parse the JSON structure from Youtube - def self.from_yt_json(container : JSON::Any) + def self.from_yt_json(container : JSON::Any) : Array(Storyboard) + # Livestream storyboards are a bit different + # TODO: document exactly how + if storyboard = container.dig?("playerLiveStoryboardSpecRenderer", "spec").try &.as_s + return [Storyboard.new( + url: URI.parse(storyboard.split("#")[0]), + width: 106, + height: 60, + count: -1, + interval: 5000, + rows: 3, + columns: 3, + images_count: -1 + )] + end + + # Split the storyboard string into chunks + # + # General format (whitespaces added for legibility): + # https://i.ytimg.com/sb//storyboard3_L$L/$N.jpg?sqp= + # | 48 # 27 # 100 # 10 # 10 # 0 # default # rs$ + # | 80 # 45 # 95 # 10 # 10 # 10000 # M$M # rs$ + # | 160 # 90 # 95 # 5 # 5 # 10000 # M$M # rs$ + # storyboards = container.dig?("playerStoryboardSpecRenderer", "spec") .try &.as_s.split("|") - if !storyboards - if storyboard = container.dig?("playerLiveStoryboardSpecRenderer", "spec").try &.as_s - return [Storyboard.new( - url: storyboard.split("#")[0], - width: 106, - height: 60, - count: -1, - interval: 5000, - storyboard_width: 3, - storyboard_height: 3, - storyboard_count: -1, - )] - end - end - - items = [] of Storyboard - - return items if !storyboards + return [] of Storyboard if !storyboards + # The base URL is the first chunk url = URI.parse(storyboards.shift) - params = HTTP::Params.parse(url.query || "") + params = url.query_params - storyboards.each_with_index do |sb, i| - width, height, count, storyboard_width, storyboard_height, interval, _, sigh = sb.split("#") - params["sigh"] = sigh - url.query = params.to_s + return storyboards.map_with_index do |sb, i| + # Separate the different storyboard parameters: + # width/height: respective dimensions, in pixels, of a single thumbnail + # count: how many thumbnails are displayed across the full video + # columns/rows: maximum amount of thumbnails that can be stuffed in a + # single image, horizontally and vertically. + # interval: interval between two thumbnails, in milliseconds + # sigh: URL cryptographic signature + width, height, count, columns, rows, interval, _, sigh = sb.split("#") width = width.to_i height = height.to_i count = count.to_i interval = interval.to_i - storyboard_width = storyboard_width.to_i - storyboard_height = storyboard_height.to_i - storyboard_count = (count / (storyboard_width * storyboard_height)).ceil.to_i + columns = columns.to_i + rows = rows.to_i - items << Storyboard.new( - url: url.to_s.sub("$L", i).sub("$N", "M$M"), + # Add the signature to the URL + params["sigh"] = sigh + url.query = params.to_s + + # Replace the template parts with what we have + url.path = url.path.sub("$L", i).sub("$N", "M$M") + + # This value represents the maximum amount of thumbnails that can fit + # in a single image. The last image (or the only one for short videos) + # will contain less thumbnails than that. + thumbnails_per_image = columns * rows + + # This value represents the total amount of storyboards required to + # hold all of the thumbnails. It can't be less than 1. + images_count = (count / thumbnails_per_image).ceil.to_i + + Storyboard.new( + url: url, width: width, height: height, count: count, interval: interval, - storyboard_width: storyboard_width, - storyboard_height: storyboard_height, - storyboard_count: storyboard_count + rows: rows, + columns: columns, + images_count: images_count, ) end - - items end end end From 7b50388eafcd458221f3deec03bf5a0829244529 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 8 Oct 2023 20:36:32 +0200 Subject: [PATCH 4/8] Storyboards: Fix broken first storyboard --- src/invidious/videos.cr | 2 +- src/invidious/videos/storyboard.cr | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 733219094..28cbb3111 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -178,7 +178,7 @@ struct Video def storyboards container = info.dig?("storyboards") || JSON::Any.new("{}") - return IV::Videos::Storyboard.from_yt_json(container) + return IV::Videos::Storyboard.from_yt_json(container, self.length_seconds) end def paid diff --git a/src/invidious/videos/storyboard.cr b/src/invidious/videos/storyboard.cr index f6df187ff..61aafe375 100644 --- a/src/invidious/videos/storyboard.cr +++ b/src/invidious/videos/storyboard.cr @@ -30,7 +30,7 @@ module Invidious::Videos end # Parse the JSON structure from Youtube - def self.from_yt_json(container : JSON::Any) : Array(Storyboard) + def self.from_yt_json(container : JSON::Any, length_seconds : Int32) : Array(Storyboard) # Livestream storyboards are a bit different # TODO: document exactly how if storyboard = container.dig?("playerLiveStoryboardSpecRenderer", "spec").try &.as_s @@ -70,8 +70,9 @@ module Invidious::Videos # columns/rows: maximum amount of thumbnails that can be stuffed in a # single image, horizontally and vertically. # interval: interval between two thumbnails, in milliseconds + # name: storyboard filename. Usually "M$M" or "default" # sigh: URL cryptographic signature - width, height, count, columns, rows, interval, _, sigh = sb.split("#") + width, height, count, columns, rows, interval, name, sigh = sb.split("#") width = width.to_i height = height.to_i @@ -85,7 +86,7 @@ module Invidious::Videos url.query = params.to_s # Replace the template parts with what we have - url.path = url.path.sub("$L", i).sub("$N", "M$M") + url.path = url.path.sub("$L", i).sub("$N", name) # This value represents the maximum amount of thumbnails that can fit # in a single image. The last image (or the only one for short videos) @@ -96,6 +97,12 @@ module Invidious::Videos # hold all of the thumbnails. It can't be less than 1. images_count = (count / thumbnails_per_image).ceil.to_i + # Compute the interval when needed (in general, that's only required + # for the first "default" storyboard). + if interval == 0 + interval = ((length_seconds / count) * 1_000).to_i + end + Storyboard.new( url: url, width: width, From a335bc0814d3253852ed5b5cf58b75d9f7b6cd70 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 20 Oct 2023 23:37:12 +0200 Subject: [PATCH 5/8] Storyboards: Fix some small logic mistakes --- src/invidious/videos/storyboard.cr | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/invidious/videos/storyboard.cr b/src/invidious/videos/storyboard.cr index 61aafe375..350126636 100644 --- a/src/invidious/videos/storyboard.cr +++ b/src/invidious/videos/storyboard.cr @@ -25,7 +25,7 @@ module Invidious::Videos authority = /(i\d?).ytimg.com/.match(@url.host.not_nil!).not_nil![1]? @proxied_url = URI.parse(HOST_URL) - @proxied_url.path = "/sb/#{authority}#{@url.path}" + @proxied_url.path = "/sb/#{authority}/#{@url.path.lchop("/sb/")}" @proxied_url.query = @url.query end @@ -60,8 +60,7 @@ module Invidious::Videos return [] of Storyboard if !storyboards # The base URL is the first chunk - url = URI.parse(storyboards.shift) - params = url.query_params + base_url = URI.parse(storyboards.shift) return storyboards.map_with_index do |sb, i| # Separate the different storyboard parameters: @@ -81,9 +80,13 @@ module Invidious::Videos columns = columns.to_i rows = rows.to_i + # Copy base URL object, so that we can modify it + url = base_url.dup + # Add the signature to the URL + params = url.query_params params["sigh"] = sigh - url.query = params.to_s + url.query_params = params # Replace the template parts with what we have url.path = url.path.sub("$L", i).sub("$N", name) From 5b05f3bd147c6cf9421587565dea2b11640f1206 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 16 Aug 2024 11:28:35 +0200 Subject: [PATCH 6/8] Storyboards: Workarounds for videojs-vtt-thumbnails The workarounds are as follow: * Unescape HTML entities * Always use 0:00:00.000 for cue start/end --- src/invidious/routes/api/v1/videos.cr | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index fb083934e..ab03df012 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -1,3 +1,5 @@ +require "html" + module Invidious::Routes::API::V1::Videos def self.videos(env) locale = env.get("preferences").as(Preferences).locale @@ -216,12 +218,14 @@ module Invidious::Routes::API::V1::Videos template_path = sb.proxied_url.path # Initialize cue timing variables + # NOTE: videojs-vtt-thumbnails gets lost when the start and end times are not 0:00:000.000 + # TODO: Use proper end time when videojs-vtt-thumbnails is fixed time_delta = sb.interval.milliseconds start_time = 0.milliseconds - end_time = time_delta - 1.milliseconds + end_time = 0.milliseconds # time_delta - 1.milliseconds # Build a VTT file for VideoJS-vtt plugin - return WebVTT.build do |vtt| + vtt_file = WebVTT.build do |vtt| sb.images_count.times do |i| # Replace the variable component part of the path work_url.path = template_path.sub("$M", i) @@ -233,12 +237,18 @@ module Invidious::Routes::API::V1::Videos vtt.cue(start_time, end_time, work_url.to_s) - start_time += time_delta - end_time += time_delta + # TODO: uncomment these when videojs-vtt-thumbnails is fixed + # start_time += time_delta + # end_time += time_delta end end end end + + # videojs-vtt-thumbnails is not compliant to the VTT specification, it + # doesn't unescape the HTML entities, so we have to do it here: + # TODO: remove this when we migrate to VideoJS 8 + return HTML.unescape(vtt_file) end def self.annotations(env) From 764965c441a789e0be417648716f575067d9201e Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 17 Aug 2024 12:20:53 +0200 Subject: [PATCH 7/8] Storyboards: Fix lint error --- src/invidious/videos/storyboard.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/videos/storyboard.cr b/src/invidious/videos/storyboard.cr index 350126636..a72c2f559 100644 --- a/src/invidious/videos/storyboard.cr +++ b/src/invidious/videos/storyboard.cr @@ -22,7 +22,7 @@ module Invidious::Videos *, @url, @width, @height, @count, @interval, @rows, @columns, @images_count ) - authority = /(i\d?).ytimg.com/.match(@url.host.not_nil!).not_nil![1]? + authority = /(i\d?).ytimg.com/.match!(@url.host.not_nil!)[1]? @proxied_url = URI.parse(HOST_URL) @proxied_url.path = "/sb/#{authority}/#{@url.path.lchop("/sb/")}" From 21ab5dc6680da3df62feed14c00104754f2479a4 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 22 Aug 2024 00:29:15 +0200 Subject: [PATCH 8/8] Storyboard: Revert cue timing "fix" --- src/invidious/routes/api/v1/videos.cr | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index ab03df012..c077b85ee 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -218,11 +218,11 @@ module Invidious::Routes::API::V1::Videos template_path = sb.proxied_url.path # Initialize cue timing variables - # NOTE: videojs-vtt-thumbnails gets lost when the start and end times are not 0:00:000.000 - # TODO: Use proper end time when videojs-vtt-thumbnails is fixed + # NOTE: videojs-vtt-thumbnails gets lost when the cue times don't overlap + # (i.e: if cue[n] end time is 1:06:25.000, cue[n+1] start time should be 1:06:25.000) time_delta = sb.interval.milliseconds start_time = 0.milliseconds - end_time = 0.milliseconds # time_delta - 1.milliseconds + end_time = time_delta # Build a VTT file for VideoJS-vtt plugin vtt_file = WebVTT.build do |vtt| @@ -237,9 +237,8 @@ module Invidious::Routes::API::V1::Videos vtt.cue(start_time, end_time, work_url.to_s) - # TODO: uncomment these when videojs-vtt-thumbnails is fixed - # start_time += time_delta - # end_time += time_delta + start_time += time_delta + end_time += time_delta end end end