@@ -802,23 +802,50 @@ class VideoRedirect < Exception
802802 end
803803end
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+ }
822849end
823850
824851def extract_video_info (video_id : String , proxy_region : String ? = nil , context_screen : String ? = nil )
@@ -871,18 +898,6 @@ 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
888903 main_results = player_response.dig?(" contents" , " twoColumnWatchNextResults" )
@@ -907,6 +922,38 @@ def extract_video_info(video_id : String, proxy_region : String? = nil, context_
907922 raise BrokenTubeException .new(" videoPrimaryInfoRenderer" ) if ! video_primary_renderer
908923 raise BrokenTubeException .new(" videoSecondaryInfoRenderer" ) if ! video_secondary_renderer
909924
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+ secondary_results.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)
956+
910957 # Likes/dislikes
911958
912959 toplevel_buttons = video_primary_renderer
0 commit comments