Storyboards: Cleanup and document code
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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/<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("|") | ||||
|  | ||||
|       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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Samantaz Fox
					Samantaz Fox