summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSamantaz Fox <coding@samantaz.fr>2023-10-08 20:29:41 +0200
committerSamantaz Fox <coding@samantaz.fr>2024-08-16 10:05:47 +0200
commitda3d58f03c9b1617f96f4caf1e348a35105dd79c (patch)
treeedab46487c64f5710333e51529d3bfd2738677da
parent8327862697774cd8076335fe2002875dd8c5a84a (diff)
downloadinvidious-da3d58f03c9b1617f96f4caf1e348a35105dd79c.tar.gz
invidious-da3d58f03c9b1617f96f4caf1e348a35105dd79c.tar.bz2
invidious-da3d58f03c9b1617f96f4caf1e348a35105dd79c.zip
Storyboards: Cleanup and document code
-rw-r--r--src/invidious/jsonify/api_v1/video_json.cr20
-rw-r--r--src/invidious/routes/api/v1/videos.cr52
-rw-r--r--src/invidious/videos/storyboard.cr114
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 44a34b18..4d12a072 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 78f91a2e..fb083934 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]
+
+ # 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
- WebVTT.build do |vtt|
- start_time = 0.milliseconds
- end_time = storyboard.interval.milliseconds
+ # Initialize cue timing variables
+ time_delta = sb.interval.milliseconds
+ start_time = 0.milliseconds
+ end_time = time_delta - 1.milliseconds
- storyboard.storyboard_count.times do |i|
- url = storyboard.url
- authority = /(i\d?).ytimg.com/.match!(url)[1]?
+ # 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)
- url = url.gsub("$M", i).gsub(%r(https://i\d?.ytimg.com/sb/), "")
- url = "#{HOST_URL}/sb/#{authority}/#{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}"
- 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)
+ vtt.cue(start_time, end_time, work_url.to_s)
- start_time += storyboard.interval.milliseconds
- end_time += storyboard.interval.milliseconds
+ 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 797fba12..f6df187f 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)
- 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
+ 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
- items = [] of Storyboard
+ # Split the storyboard string into chunks
+ #
+ # General format (whitespaces added for legibility):
+ # https://i.ytimg.com/sb/<video_id>/storyboard3_L$L/$N.jpg?sqp=<sig0>
+ # | 48 # 27 # 100 # 10 # 10 # 0 # default # rs$<sig1>
+ # | 80 # 45 # 95 # 10 # 10 # 10000 # M$M # rs$<sig2>
+ # | 160 # 90 # 95 # 5 # 5 # 10000 # M$M # rs$<sig3>
+ #
+ storyboards = container.dig?("playerStoryboardSpecRenderer", "spec")
+ .try &.as_s.split("|")
- 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
+
+ # Add the signature to the URL
+ params["sigh"] = sigh
+ url.query = params.to_s
- items << Storyboard.new(
- url: url.to_s.sub("$L", i).sub("$N", "M$M"),
+ # 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