Merge pull request #2856 from SamantazFox/fix-related-videos
Fix related videos
This commit is contained in:
commit
bd221b7b2c
8
src/invidious/exceptions.cr
Normal file
8
src/invidious/exceptions.cr
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# Exception used to hold the name of the missing item
|
||||||
|
# Should be used in all parsing functions
|
||||||
|
class BrokenTubeException < InfoException
|
||||||
|
getter element : String
|
||||||
|
|
||||||
|
def initialize(@element)
|
||||||
|
end
|
||||||
|
end
|
|
@ -446,7 +446,7 @@ struct Video
|
||||||
end
|
end
|
||||||
|
|
||||||
json.field "author", rv["author"]
|
json.field "author", rv["author"]
|
||||||
json.field "authorUrl", rv["author_url"]?
|
json.field "authorUrl", "/channel/#{rv["ucid"]?}"
|
||||||
json.field "authorId", rv["ucid"]?
|
json.field "authorId", rv["ucid"]?
|
||||||
if rv["author_thumbnail"]?
|
if rv["author_thumbnail"]?
|
||||||
json.field "authorThumbnails" do
|
json.field "authorThumbnails" do
|
||||||
|
@ -455,7 +455,7 @@ struct Video
|
||||||
|
|
||||||
qualities.each do |quality|
|
qualities.each do |quality|
|
||||||
json.object do
|
json.object do
|
||||||
json.field "url", rv["author_thumbnail"]?.try &.gsub(/s\d+-/, "s#{quality}-")
|
json.field "url", rv["author_thumbnail"].gsub(/s\d+-/, "s#{quality}-")
|
||||||
json.field "width", quality
|
json.field "width", quality
|
||||||
json.field "height", quality
|
json.field "height", quality
|
||||||
end
|
end
|
||||||
|
@ -465,7 +465,7 @@ struct Video
|
||||||
end
|
end
|
||||||
|
|
||||||
json.field "lengthSeconds", rv["length_seconds"]?.try &.to_i
|
json.field "lengthSeconds", rv["length_seconds"]?.try &.to_i
|
||||||
json.field "viewCountText", rv["short_view_count_text"]?
|
json.field "viewCountText", rv["short_view_count"]?
|
||||||
json.field "viewCount", rv["view_count"]?.try &.empty? ? nil : rv["view_count"].to_i64
|
json.field "viewCount", rv["view_count"]?.try &.empty? ? nil : rv["view_count"].to_i64
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -802,23 +802,50 @@ class VideoRedirect < Exception
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_related(r : JSON::Any) : JSON::Any?
|
# Use to parse both "compactVideoRenderer" and "endScreenVideoRenderer".
|
||||||
# TODO: r["endScreenPlaylistRenderer"], etc.
|
# The former is preferred as it has more videos in it. The second has
|
||||||
return if !r["endScreenVideoRenderer"]?
|
# the same 11 first entries as the compact rendered.
|
||||||
r = r["endScreenVideoRenderer"].as_h
|
#
|
||||||
|
# TODO: "compactRadioRenderer" (Mix) and
|
||||||
|
def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
|
||||||
|
return nil if !related["videoId"]?
|
||||||
|
|
||||||
return if !r["lengthInSeconds"]?
|
# The compact renderer has video length in seconds, where the end
|
||||||
|
# screen rendered has a full text version ("42:40")
|
||||||
|
length = related["lengthInSeconds"]?.try &.as_i.to_s
|
||||||
|
length ||= related.dig?("lengthText", "simpleText").try do |box|
|
||||||
|
decode_length_seconds(box.as_s).to_s
|
||||||
|
end
|
||||||
|
|
||||||
rv = {} of String => JSON::Any
|
# Both have "short", so the "long" option shouldn't be required
|
||||||
rv["author"] = r["shortBylineText"]["runs"][0]?.try &.["text"] || JSON::Any.new("")
|
channel_info = (related["shortBylineText"]? || related["longBylineText"]?)
|
||||||
rv["ucid"] = r["shortBylineText"]["runs"][0]?.try &.["navigationEndpoint"]["browseEndpoint"]["browseId"] || JSON::Any.new("")
|
.try &.dig?("runs", 0)
|
||||||
rv["author_url"] = JSON::Any.new("/channel/#{rv["ucid"]}")
|
|
||||||
rv["length_seconds"] = JSON::Any.new(r["lengthInSeconds"].as_i.to_s)
|
author = channel_info.try &.dig?("text")
|
||||||
rv["title"] = r["title"]["simpleText"]
|
ucid = channel_info.try { |ci| HelperExtractors.get_browse_id(ci) }
|
||||||
rv["short_view_count_text"] = JSON::Any.new(r["shortViewCountText"]?.try &.["simpleText"]?.try &.as_s || "")
|
|
||||||
rv["view_count"] = JSON::Any.new(r["title"]["accessibility"]?.try &.["accessibilityData"]["label"].as_s.match(/(?<views>[1-9](\d+,?)*) views/).try &.["views"].gsub(/\D/, "") || "")
|
# "4,088,033 views", only available on compact renderer
|
||||||
rv["id"] = r["videoId"]
|
# and when video is not a livestream
|
||||||
JSON::Any.new(rv)
|
view_count = related.dig?("viewCountText", "simpleText")
|
||||||
|
.try &.as_s.gsub(/\D/, "")
|
||||||
|
|
||||||
|
short_view_count = related.try do |r|
|
||||||
|
HelperExtractors.get_short_view_count(r).to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
LOGGER.trace("parse_related_video: Found \"watchNextEndScreenRenderer\" container")
|
||||||
|
|
||||||
|
# TODO: when refactoring video types, make a struct for related videos
|
||||||
|
# or reuse an existing type, if that fits.
|
||||||
|
return {
|
||||||
|
"id" => related["videoId"],
|
||||||
|
"title" => related["title"]["simpleText"],
|
||||||
|
"author" => author || JSON::Any.new(""),
|
||||||
|
"ucid" => JSON::Any.new(ucid || ""),
|
||||||
|
"length_seconds" => JSON::Any.new(length || "0"),
|
||||||
|
"view_count" => JSON::Any.new(view_count || "0"),
|
||||||
|
"short_view_count" => JSON::Any.new(short_view_count || "0"),
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def extract_video_info(video_id : String, proxy_region : String? = nil, context_screen : String? = nil)
|
def extract_video_info(video_id : String, proxy_region : String? = nil, context_screen : String? = nil)
|
||||||
|
@ -871,31 +898,62 @@ def extract_video_info(video_id : String, proxy_region : String? = nil, context_
|
||||||
params[f] = player_response[f] if player_response[f]?
|
params[f] = player_response[f] if player_response[f]?
|
||||||
end
|
end
|
||||||
|
|
||||||
params["relatedVideos"] = (
|
|
||||||
player_response
|
|
||||||
.dig?("playerOverlays", "playerOverlayRenderer", "endScreen", "watchNextEndScreenRenderer", "results")
|
|
||||||
.try &.as_a.compact_map { |r| parse_related r } || \
|
|
||||||
player_response
|
|
||||||
.dig?("webWatchNextResponseExtensionData", "relatedVideoArgs")
|
|
||||||
.try &.as_s.split(",").map { |r|
|
|
||||||
r = HTTP::Params.parse(r).to_h
|
|
||||||
JSON::Any.new(Hash.zip(r.keys, r.values.map { |v| JSON::Any.new(v) }))
|
|
||||||
}
|
|
||||||
).try { |a| JSON::Any.new(a) } || JSON::Any.new([] of JSON::Any)
|
|
||||||
|
|
||||||
# Top level elements
|
# Top level elements
|
||||||
|
|
||||||
primary_results = player_response
|
main_results = player_response.dig?("contents", "twoColumnWatchNextResults")
|
||||||
.dig?("contents", "twoColumnWatchNextResults", "results", "results", "contents")
|
|
||||||
|
raise BrokenTubeException.new("twoColumnWatchNextResults") if !main_results
|
||||||
|
|
||||||
|
primary_results = main_results.dig?("results", "results", "contents")
|
||||||
|
secondary_results = main_results
|
||||||
|
.dig?("secondaryResults", "secondaryResults", "results")
|
||||||
|
|
||||||
|
raise BrokenTubeException.new("results") if !primary_results
|
||||||
|
raise BrokenTubeException.new("secondaryResults") if !secondary_results
|
||||||
|
|
||||||
video_primary_renderer = primary_results
|
video_primary_renderer = primary_results
|
||||||
.try &.as_a.find(&.["videoPrimaryInfoRenderer"]?)
|
.as_a.find(&.["videoPrimaryInfoRenderer"]?)
|
||||||
.try &.["videoPrimaryInfoRenderer"]
|
.try &.["videoPrimaryInfoRenderer"]
|
||||||
|
|
||||||
video_secondary_renderer = primary_results
|
video_secondary_renderer = primary_results
|
||||||
.try &.as_a.find(&.["videoSecondaryInfoRenderer"]?)
|
.as_a.find(&.["videoSecondaryInfoRenderer"]?)
|
||||||
.try &.["videoSecondaryInfoRenderer"]
|
.try &.["videoSecondaryInfoRenderer"]
|
||||||
|
|
||||||
|
raise BrokenTubeException.new("videoPrimaryInfoRenderer") if !video_primary_renderer
|
||||||
|
raise BrokenTubeException.new("videoSecondaryInfoRenderer") if !video_secondary_renderer
|
||||||
|
|
||||||
|
# Related videos
|
||||||
|
|
||||||
|
LOGGER.debug("extract_video_info: parsing related videos...")
|
||||||
|
|
||||||
|
related = [] of JSON::Any
|
||||||
|
|
||||||
|
# Parse "compactVideoRenderer" items (under secondary results)
|
||||||
|
secondary_results.as_a.each do |element|
|
||||||
|
if item = element["compactVideoRenderer"]?
|
||||||
|
related_video = parse_related_video(item)
|
||||||
|
related << JSON::Any.new(related_video) if related_video
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# If nothing was found previously, fall back to end screen renderer
|
||||||
|
if related.empty?
|
||||||
|
# Container for "endScreenVideoRenderer" items
|
||||||
|
player_overlays = player_response.dig?(
|
||||||
|
"playerOverlays", "playerOverlayRenderer",
|
||||||
|
"endScreen", "watchNextEndScreenRenderer", "results"
|
||||||
|
)
|
||||||
|
|
||||||
|
player_overlays.try &.as_a.each do |element|
|
||||||
|
if item = element["endScreenVideoRenderer"]?
|
||||||
|
related_video = parse_related_video(item)
|
||||||
|
related << JSON::Any.new(related_video) if related_video
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
params["relatedVideos"] = JSON::Any.new(related)
|
||||||
|
|
||||||
# Likes/dislikes
|
# Likes/dislikes
|
||||||
|
|
||||||
toplevel_buttons = video_primary_renderer
|
toplevel_buttons = video_primary_renderer
|
||||||
|
|
|
@ -321,11 +321,11 @@ we're going to need to do it here in order to allow for translations.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pure-u-10-24" style="text-align:right">
|
<div class="pure-u-10-24" style="text-align:right">
|
||||||
<% if views = rv["short_view_count_text"]?.try &.delete(", views watching") %>
|
<b class="width:100%"><%=
|
||||||
<% if !views.empty? %>
|
views = rv["view_count"]?.try &.to_i?
|
||||||
<b class="width:100%"><%= translate_count(locale, "generic_views_count", views.to_i? || 0) %></b>
|
views ||= rv["view_count_short"]?.try { |x| short_text_to_number(x) }
|
||||||
<% end %>
|
translate_count(locale, "generic_views_count", views || 0, NumberFormatting::Short)
|
||||||
<% end %>
|
%></b>
|
||||||
</div>
|
</div>
|
||||||
</h5>
|
</h5>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -505,7 +505,7 @@ end
|
||||||
#
|
#
|
||||||
# Mostly used to extract out repeated structures to deal with code
|
# Mostly used to extract out repeated structures to deal with code
|
||||||
# repetition.
|
# repetition.
|
||||||
private module HelperExtractors
|
module HelperExtractors
|
||||||
# Retrieves the amount of videos present within the given InnerTube data.
|
# Retrieves the amount of videos present within the given InnerTube data.
|
||||||
#
|
#
|
||||||
# Returns a 0 when it's unable to do so
|
# Returns a 0 when it's unable to do so
|
||||||
|
@ -519,6 +519,20 @@ private module HelperExtractors
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Retrieves the amount of views/viewers a video has.
|
||||||
|
# Seems to be used on related videos only
|
||||||
|
#
|
||||||
|
# Returns "0" when unable to parse
|
||||||
|
def self.get_short_view_count(container : JSON::Any) : String
|
||||||
|
box = container["shortViewCountText"]?
|
||||||
|
return "0" if !box
|
||||||
|
|
||||||
|
# Simpletext: "4M views"
|
||||||
|
# runs: {"text": "1.1K"},{"text":" watching"}
|
||||||
|
return box["simpleText"]?.try &.as_s.sub(" views", "") ||
|
||||||
|
box.dig?("runs", 0, "text").try &.as_s || "0"
|
||||||
|
end
|
||||||
|
|
||||||
# Retrieve lowest quality thumbnail from InnerTube data
|
# Retrieve lowest quality thumbnail from InnerTube data
|
||||||
#
|
#
|
||||||
# TODO allow configuration of image quality (-1 is highest)
|
# TODO allow configuration of image quality (-1 is highest)
|
||||||
|
|
Loading…
Reference in a new issue