Skip to content

Commit bd221b7

Browse files
authored
Merge pull request #2856 from SamantazFox/fix-related-videos
Fix related videos
2 parents 0ca3337 + ba37259 commit bd221b7

File tree

4 files changed

+124
-44
lines changed

4 files changed

+124
-44
lines changed

src/invidious/exceptions.cr

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Exception used to hold the name of the missing item
2+
# Should be used in all parsing functions
3+
class BrokenTubeException < InfoException
4+
getter element : String
5+
6+
def initialize(@element)
7+
end
8+
end

src/invidious/videos.cr

Lines changed: 96 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,7 @@ struct Video
446446
end
447447

448448
json.field "author", rv["author"]
449-
json.field "authorUrl", rv["author_url"]?
449+
json.field "authorUrl", "/channel/#{rv["ucid"]?}"
450450
json.field "authorId", rv["ucid"]?
451451
if rv["author_thumbnail"]?
452452
json.field "authorThumbnails" do
@@ -455,7 +455,7 @@ struct Video
455455

456456
qualities.each do |quality|
457457
json.object do
458-
json.field "url", rv["author_thumbnail"]?.try &.gsub(/s\d+-/, "s#{quality}-")
458+
json.field "url", rv["author_thumbnail"].gsub(/s\d+-/, "s#{quality}-")
459459
json.field "width", quality
460460
json.field "height", quality
461461
end
@@ -465,7 +465,7 @@ struct Video
465465
end
466466

467467
json.field "lengthSeconds", rv["length_seconds"]?.try &.to_i
468-
json.field "viewCountText", rv["short_view_count_text"]?
468+
json.field "viewCountText", rv["short_view_count"]?
469469
json.field "viewCount", rv["view_count"]?.try &.empty? ? nil : rv["view_count"].to_i64
470470
end
471471
end
@@ -802,23 +802,50 @@ class VideoRedirect < Exception
802802
end
803803
end
804804

805-
def parse_related(r : JSON::Any) : JSON::Any?
806-
# TODO: r["endScreenPlaylistRenderer"], etc.
807-
return if !r["endScreenVideoRenderer"]?
808-
r = r["endScreenVideoRenderer"].as_h
809-
810-
return if !r["lengthInSeconds"]?
811-
812-
rv = {} of String => JSON::Any
813-
rv["author"] = r["shortBylineText"]["runs"][0]?.try &.["text"] || JSON::Any.new("")
814-
rv["ucid"] = r["shortBylineText"]["runs"][0]?.try &.["navigationEndpoint"]["browseEndpoint"]["browseId"] || JSON::Any.new("")
815-
rv["author_url"] = JSON::Any.new("/channel/#{rv["ucid"]}")
816-
rv["length_seconds"] = JSON::Any.new(r["lengthInSeconds"].as_i.to_s)
817-
rv["title"] = r["title"]["simpleText"]
818-
rv["short_view_count_text"] = JSON::Any.new(r["shortViewCountText"]?.try &.["simpleText"]?.try &.as_s || "")
819-
rv["view_count"] = JSON::Any.new(r["title"]["accessibility"]?.try &.["accessibilityData"]["label"].as_s.match(/(?<views>[1-9](\d+,?)*) views/).try &.["views"].gsub(/\D/, "") || "")
820-
rv["id"] = r["videoId"]
821-
JSON::Any.new(rv)
805+
# Use to parse both "compactVideoRenderer" and "endScreenVideoRenderer".
806+
# The former is preferred as it has more videos in it. The second has
807+
# the same 11 first entries as the compact rendered.
808+
#
809+
# TODO: "compactRadioRenderer" (Mix) and
810+
def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
811+
return nil if !related["videoId"]?
812+
813+
# The compact renderer has video length in seconds, where the end
814+
# screen rendered has a full text version ("42:40")
815+
length = related["lengthInSeconds"]?.try &.as_i.to_s
816+
length ||= related.dig?("lengthText", "simpleText").try do |box|
817+
decode_length_seconds(box.as_s).to_s
818+
end
819+
820+
# Both have "short", so the "long" option shouldn't be required
821+
channel_info = (related["shortBylineText"]? || related["longBylineText"]?)
822+
.try &.dig?("runs", 0)
823+
824+
author = channel_info.try &.dig?("text")
825+
ucid = channel_info.try { |ci| HelperExtractors.get_browse_id(ci) }
826+
827+
# "4,088,033 views", only available on compact renderer
828+
# and when video is not a livestream
829+
view_count = related.dig?("viewCountText", "simpleText")
830+
.try &.as_s.gsub(/\D/, "")
831+
832+
short_view_count = related.try do |r|
833+
HelperExtractors.get_short_view_count(r).to_s
834+
end
835+
836+
LOGGER.trace("parse_related_video: Found \"watchNextEndScreenRenderer\" container")
837+
838+
# TODO: when refactoring video types, make a struct for related videos
839+
# or reuse an existing type, if that fits.
840+
return {
841+
"id" => related["videoId"],
842+
"title" => related["title"]["simpleText"],
843+
"author" => author || JSON::Any.new(""),
844+
"ucid" => JSON::Any.new(ucid || ""),
845+
"length_seconds" => JSON::Any.new(length || "0"),
846+
"view_count" => JSON::Any.new(view_count || "0"),
847+
"short_view_count" => JSON::Any.new(short_view_count || "0"),
848+
}
822849
end
823850

824851
def extract_video_info(video_id : String, proxy_region : String? = nil, context_screen : String? = nil)
@@ -871,30 +898,61 @@ def extract_video_info(video_id : String, proxy_region : String? = nil, context_
871898
params[f] = player_response[f] if player_response[f]?
872899
end
873900

874-
params["relatedVideos"] = (
875-
player_response
876-
.dig?("playerOverlays", "playerOverlayRenderer", "endScreen", "watchNextEndScreenRenderer", "results")
877-
.try &.as_a.compact_map { |r| parse_related r } || \
878-
player_response
879-
.dig?("webWatchNextResponseExtensionData", "relatedVideoArgs")
880-
.try &.as_s.split(",").map { |r|
881-
r = HTTP::Params.parse(r).to_h
882-
JSON::Any.new(Hash.zip(r.keys, r.values.map { |v| JSON::Any.new(v) }))
883-
}
884-
).try { |a| JSON::Any.new(a) } || JSON::Any.new([] of JSON::Any)
885-
886901
# Top level elements
887902

888-
primary_results = player_response
889-
.dig?("contents", "twoColumnWatchNextResults", "results", "results", "contents")
903+
main_results = player_response.dig?("contents", "twoColumnWatchNextResults")
904+
905+
raise BrokenTubeException.new("twoColumnWatchNextResults") if !main_results
906+
907+
primary_results = main_results.dig?("results", "results", "contents")
908+
secondary_results = main_results
909+
.dig?("secondaryResults", "secondaryResults", "results")
910+
911+
raise BrokenTubeException.new("results") if !primary_results
912+
raise BrokenTubeException.new("secondaryResults") if !secondary_results
890913

891914
video_primary_renderer = primary_results
892-
.try &.as_a.find(&.["videoPrimaryInfoRenderer"]?)
893-
.try &.["videoPrimaryInfoRenderer"]
915+
.as_a.find(&.["videoPrimaryInfoRenderer"]?)
916+
.try &.["videoPrimaryInfoRenderer"]
894917

895918
video_secondary_renderer = primary_results
896-
.try &.as_a.find(&.["videoSecondaryInfoRenderer"]?)
897-
.try &.["videoSecondaryInfoRenderer"]
919+
.as_a.find(&.["videoSecondaryInfoRenderer"]?)
920+
.try &.["videoSecondaryInfoRenderer"]
921+
922+
raise BrokenTubeException.new("videoPrimaryInfoRenderer") if !video_primary_renderer
923+
raise BrokenTubeException.new("videoSecondaryInfoRenderer") if !video_secondary_renderer
924+
925+
# Related videos
926+
927+
LOGGER.debug("extract_video_info: parsing related videos...")
928+
929+
related = [] of JSON::Any
930+
931+
# Parse "compactVideoRenderer" items (under secondary results)
932+
secondary_results.as_a.each do |element|
933+
if item = element["compactVideoRenderer"]?
934+
related_video = parse_related_video(item)
935+
related << JSON::Any.new(related_video) if related_video
936+
end
937+
end
938+
939+
# If nothing was found previously, fall back to end screen renderer
940+
if related.empty?
941+
# Container for "endScreenVideoRenderer" items
942+
player_overlays = player_response.dig?(
943+
"playerOverlays", "playerOverlayRenderer",
944+
"endScreen", "watchNextEndScreenRenderer", "results"
945+
)
946+
947+
player_overlays.try &.as_a.each do |element|
948+
if item = element["endScreenVideoRenderer"]?
949+
related_video = parse_related_video(item)
950+
related << JSON::Any.new(related_video) if related_video
951+
end
952+
end
953+
end
954+
955+
params["relatedVideos"] = JSON::Any.new(related)
898956

899957
# Likes/dislikes
900958

src/invidious/views/watch.ecr

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -321,11 +321,11 @@ we're going to need to do it here in order to allow for translations.
321321
</div>
322322
323323
<div class="pure-u-10-24" style="text-align:right">
324-
<% if views = rv["short_view_count_text"]?.try &.delete(", views watching") %>
325-
<% if !views.empty? %>
326-
<b class="width:100%"><%= translate_count(locale, "generic_views_count", views.to_i? || 0) %></b>
327-
<% end %>
328-
<% end %>
324+
<b class="width:100%"><%=
325+
views = rv["view_count"]?.try &.to_i?
326+
views ||= rv["view_count_short"]?.try { |x| short_text_to_number(x) }
327+
translate_count(locale, "generic_views_count", views || 0, NumberFormatting::Short)
328+
%></b>
329329
</div>
330330
</h5>
331331
</a>

src/invidious/yt_backend/extractors.cr

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,7 @@ end
505505
#
506506
# Mostly used to extract out repeated structures to deal with code
507507
# repetition.
508-
private module HelperExtractors
508+
module HelperExtractors
509509
# Retrieves the amount of videos present within the given InnerTube data.
510510
#
511511
# Returns a 0 when it's unable to do so
@@ -519,6 +519,20 @@ private module HelperExtractors
519519
end
520520
end
521521

522+
# Retrieves the amount of views/viewers a video has.
523+
# Seems to be used on related videos only
524+
#
525+
# Returns "0" when unable to parse
526+
def self.get_short_view_count(container : JSON::Any) : String
527+
box = container["shortViewCountText"]?
528+
return "0" if !box
529+
530+
# Simpletext: "4M views"
531+
# runs: {"text": "1.1K"},{"text":" watching"}
532+
return box["simpleText"]?.try &.as_s.sub(" views", "") ||
533+
box.dig?("runs", 0, "text").try &.as_s || "0"
534+
end
535+
522536
# Retrieve lowest quality thumbnail from InnerTube data
523537
#
524538
# TODO allow configuration of image quality (-1 is highest)

0 commit comments

Comments
 (0)