From 62a4c82e95212a45608708dddd967072b478157f Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Thu, 11 Apr 2019 17:00:00 -0500 Subject: [PATCH] Add storyboards and fix image caching --- src/invidious.cr | 65 +++++++++++++++++++++++++++++-- src/invidious/videos.cr | 86 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 3 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 83b389a78..f393e2d61 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -2973,6 +2973,9 @@ get "/api/v1/videos/:id" do |env| json.field "videoThumbnails" do generate_thumbnails(json, video.id, config, Kemal.config) end + json.field "storyboards" do + generate_storyboards(json, video.storyboards, config, Kemal.config) + end video.description, description = html_to_content(video.description) @@ -4348,7 +4351,7 @@ get "/videoplayback" do |env| url = "/videoplayback?#{query_params.to_s}" headers = HTTP::Headers.new - {"Accept", "Accept-Encoding", "Connection", "Range"}.each do |header| + {"Accept", "Accept-Encoding", "Cache-Control", "Connection", "If-None-Match", "Range"}.each do |header| if env.request.headers[header]? headers[header] = env.request.headers[header] end @@ -4445,7 +4448,63 @@ get "/ggpht/*" do |env| url = env.request.path.lchop("/ggpht") headers = HTTP::Headers.new - {"Range", "Accept", "Accept-Encoding"}.each do |header| + {"Accept", "Accept-Encoding", "Cache-Control", "Connection", "If-None-Match", "Range"}.each do |header| + if env.request.headers[header]? + headers[header] = env.request.headers[header] + end + end + + client.get(url, headers) do |response| + env.response.status_code = response.status_code + response.headers.each do |key, value| + env.response.headers[key] = value + end + + if response.status_code == 304 + break + end + + chunk_size = 4096 + size = 1 + if response.headers.includes_word?("Content-Encoding", "gzip") + Gzip::Writer.open(env.response) do |deflate| + until size == 0 + size = IO.copy(response.body_io, deflate) + env.response.flush + end + end + elsif response.headers.includes_word?("Content-Encoding", "deflate") + Flate::Writer.open(env.response) do |deflate| + until size == 0 + size = IO.copy(response.body_io, deflate) + env.response.flush + end + end + else + until size == 0 + size = IO.copy(response.body_io, env.response, chunk_size) + env.response.flush + end + end + end +end + +get "/sb/:id/:storyboard/:index" do |env| + id = env.params.url["id"] + storyboard = env.params.url["storyboard"] + index = env.params.url["index"] + + if storyboard.starts_with? "storyboard_live" + host = "https://i.ytimg.com" + else + host = "https://i9.ytimg.com" + end + client = make_client(URI.parse(host)) + + url = "/sb/#{id}/#{storyboard}/#{index}?#{env.params.query}" + + headers = HTTP::Headers.new + {"Accept", "Accept-Encoding", "Cache-Control", "Connection", "If-None-Match", "Range"}.each do |header| if env.request.headers[header]? headers[header] = env.request.headers[header] end @@ -4504,7 +4563,7 @@ get "/vi/:id/:name" do |env| url = "/vi/#{id}/#{name}" headers = HTTP::Headers.new - {"Range", "Accept", "Accept-Encoding"}.each do |header| + {"Accept", "Accept-Encoding", "Cache-Control", "Connection", "If-None-Match", "Range"}.each do |header| if env.request.headers[header]? headers[header] = env.request.headers[header] end diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index beaa1330a..9e273a96e 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -473,6 +473,75 @@ struct Video return @player_json.not_nil! end + def storyboards + storyboards = self.player_response["storyboards"]? + .try &.as_h + .try &.["playerStoryboardSpecRenderer"]? + + if !storyboards + storyboards = self.player_response["storyboards"]? + .try &.as_h + .try &.["playerLiveStoryboardSpecRenderer"]? + + if storyboard = storyboards.try &.["spec"]? + .try &.as_s + return [{ + url: storyboard.split("#")[0].sub("M$M", "$N"), + width: 106, + height: 60, + count: -1, + interval: 5000, + storyboard_width: 3, + storyboard_height: 3, + storyboard_count: -1, + }] + end + end + + storyboards = storyboards.try &.["spec"]? + .try &.as_s.split("|") + + items = [] of NamedTuple( + url: String, + width: Int32, + height: Int32, + count: Int32, + interval: Int32, + storyboard_width: Int32, + storyboard_height: Int32, + storyboard_count: Int32) + + if !storyboards + return items + end + + url = storyboards.shift + + storyboards.each_with_index do |storyboard, i| + width, height, count, storyboard_width, storyboard_height, interval, _, sigh = storyboard.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 + + items << { + url: "#{url}&sigh=#{sigh}".sub("$L", i), + width: width, + height: height, + count: count, + interval: interval, + storyboard_width: storyboard_width, + storyboard_height: storyboard_height, + storyboard_count: (count.to_f / (storyboard_width.to_f * storyboard_height.to_f)).ceil.to_i, + } + end + + items + end + def paid reason = self.player_response["playabilityStatus"]?.try &.["reason"]? @@ -1062,3 +1131,20 @@ def generate_thumbnails(json, id, config, kemal_config) end end end + +def generate_storyboards(json, storyboards, config, kemal_config) + json.array do + storyboards.each do |storyboard| + json.object do + json.field "url", 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 +end