mirror of
				https://github.com/iv-org/invidious.git
				synced 2025-10-31 04:32:02 +00:00 
			
		
		
		
	Merge pull request #2856 from SamantazFox/fix-related-videos
Fix related videos
This commit is contained in:
		
							
								
								
									
										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,30 +898,61 @@ 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 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Samantaz Fox
					Samantaz Fox