@@ -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
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,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
0 commit comments