mirror of
				https://gitea.invidious.io/iv-org/invidious
				synced 2025-06-05 23:29:12 +02:00 
			
		
		
		
	Search: Add hashtag result (#3989)
This commit is contained in:
		
							
								
								
									
										9
									
								
								assets/hashtag.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								assets/hashtag.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <svg width="128" height="128" viewBox="0 0 128 128" version="1.1" id="svg5" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"> | ||||
|   <g> | ||||
|     <rect fill="#c84fff" width="128" height="128" x="0" y="0" /> | ||||
|     <g aria-label="#" transform="matrix(1.1326954,0,0,1.1326954,-20.255282,-23.528147)"> | ||||
|       <path d="m 87.780593,70.524217 -2.624999,13.666661 h 11.666662 v 5.708331 H 84.030595 L 80.61393,107.73253 H 74.488932 L 77.988931,89.899209 H 65.863936 L 62.447271,107.73253 H 56.447273 L 59.697272,89.899209 H 48.947276 V 84.190878 H 60.822271 L 63.530603,70.524217 H 52.113942 V 64.815886 H 64.57227 l 3.416665,-17.999993 h 6.124997 l -3.416665,17.999993 h 12.208328 l 3.499999,-17.999993 h 5.999997 l -3.499998,17.999993 h 10.916662 v 5.708331 z M 66.947269,84.190878 H 79.072264 L 81.738929,70.524217 H 69.613934 Z" /> | ||||
|     </g> | ||||
|   </g> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 918 B | 
| @@ -1,4 +1,6 @@ | ||||
| { | ||||
|     "generic_channels_count": "{{count}} channel", | ||||
|     "generic_channels_count_plural": "{{count}} channels", | ||||
|     "generic_views_count": "{{count}} view", | ||||
|     "generic_views_count_plural": "{{count}} views", | ||||
|     "generic_videos_count": "{{count}} video", | ||||
|   | ||||
| @@ -1,4 +1,6 @@ | ||||
| { | ||||
|     "generic_channels_count": "{{count}} chaîne", | ||||
|     "generic_channels_count_plural": "{{count}} chaînes", | ||||
|     "generic_views_count": "{{count}} vue", | ||||
|     "generic_views_count_plural": "{{count}} vues", | ||||
|     "generic_videos_count": "{{count}} vidéo", | ||||
|   | ||||
| @@ -232,6 +232,25 @@ struct SearchChannel | ||||
|   end | ||||
| end | ||||
|  | ||||
| struct SearchHashtag | ||||
|   include DB::Serializable | ||||
|  | ||||
|   property title : String | ||||
|   property url : String | ||||
|   property video_count : Int64 | ||||
|   property channel_count : Int64 | ||||
|  | ||||
|   def to_json(locale : String?, json : JSON::Builder) | ||||
|     json.object do | ||||
|       json.field "type", "hashtag" | ||||
|       json.field "title", self.title | ||||
|       json.field "url", self.url | ||||
|       json.field "videoCount", self.video_count | ||||
|       json.field "channelCount", self.channel_count | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| class Category | ||||
|   include DB::Serializable | ||||
|  | ||||
| @@ -274,4 +293,4 @@ struct Continuation | ||||
|   end | ||||
| end | ||||
|  | ||||
| alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist | Category | ||||
| alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist | SearchHashtag | Category | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| <%- | ||||
|   thin_mode = env.get("preferences").as(Preferences).thin_mode | ||||
|   item_watched = !item.is_a?(SearchChannel | SearchPlaylist | InvidiousPlaylist | Category) && env.get?("user").try &.as(User).watched.index(item.id) != nil | ||||
|   item_watched = !item.is_a?(SearchChannel | SearchHashtag | SearchPlaylist | InvidiousPlaylist | Category) && env.get?("user").try &.as(User).watched.index(item.id) != nil | ||||
|   author_verified = item.responds_to?(:author_verified) && item.author_verified | ||||
| -%> | ||||
|  | ||||
| @@ -29,6 +29,30 @@ | ||||
|             <p><%= translate_count(locale, "generic_subscribers_count", item.subscriber_count, NumberFormatting::Separator) %></p> | ||||
|             <% if !item.auto_generated %><p><%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %></p><% end %> | ||||
|             <h5><%= item.description_html %></h5> | ||||
|         <% when SearchHashtag %> | ||||
|             <% if !thin_mode %> | ||||
|                 <a tabindex="-1" href="<%= item.url %>"> | ||||
|                     <center><img style="width:56.25%" src="/hashtag.svg" alt="" /></center> | ||||
|                 </a> | ||||
|             <%- else -%> | ||||
|                 <div class="thumbnail-placeholder" style="width:56.25%"></div> | ||||
|             <% end %> | ||||
|  | ||||
|             <div class="video-card-row"> | ||||
|                 <div class="flex-left"><a href="<%= item.url %>"><%= HTML.escape(item.title) %></a></div> | ||||
|             </div> | ||||
|  | ||||
|             <div class="video-card-row"> | ||||
|                 <%- if item.video_count != 0 -%> | ||||
|                     <p><%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %></p> | ||||
|                 <%- end -%> | ||||
|             </div> | ||||
|  | ||||
|             <div class="video-card-row"> | ||||
|                 <%- if item.channel_count != 0 -%> | ||||
|                     <p><%= translate_count(locale, "generic_channels_count", item.channel_count, NumberFormatting::Separator) %></p> | ||||
|                 <%- end -%> | ||||
|             </div> | ||||
|         <% when SearchPlaylist, InvidiousPlaylist %> | ||||
|             <%- | ||||
|               if item.id.starts_with? "RD" | ||||
|   | ||||
| @@ -11,15 +11,16 @@ private ITEM_CONTAINER_EXTRACTOR = { | ||||
| } | ||||
|  | ||||
| private ITEM_PARSERS = { | ||||
|   Parsers::RichItemRendererParser, | ||||
|   Parsers::VideoRendererParser, | ||||
|   Parsers::ChannelRendererParser, | ||||
|   Parsers::GridPlaylistRendererParser, | ||||
|   Parsers::PlaylistRendererParser, | ||||
|   Parsers::CategoryRendererParser, | ||||
|   Parsers::RichItemRendererParser, | ||||
|   Parsers::ReelItemRendererParser, | ||||
|   Parsers::ItemSectionRendererParser, | ||||
|   Parsers::ContinuationItemRendererParser, | ||||
|   Parsers::HashtagRendererParser, | ||||
| } | ||||
|  | ||||
| private alias InitialData = Hash(String, JSON::Any) | ||||
| @@ -210,6 +211,56 @@ private module Parsers | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   # Parses an Innertube `hashtagTileRenderer` into a `SearchHashtag`. | ||||
|   # Returns `nil` when the given object is not a `hashtagTileRenderer`. | ||||
|   # | ||||
|   # A `hashtagTileRenderer` is a kind of search result. | ||||
|   # It can be found when searching for any hashtag (e.g "#hi" or "#shorts") | ||||
|   module HashtagRendererParser | ||||
|     def self.process(item : JSON::Any, author_fallback : AuthorFallback) | ||||
|       if item_contents = item["hashtagTileRenderer"]? | ||||
|         return self.parse(item_contents) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     private def self.parse(item_contents) | ||||
|       title = extract_text(item_contents["hashtag"]).not_nil! # E.g "#hi" | ||||
|  | ||||
|       # E.g "/hashtag/hi" | ||||
|       url = item_contents.dig?("onTapCommand", "commandMetadata", "webCommandMetadata", "url").try &.as_s | ||||
|       url ||= URI.encode_path("/hashtag/#{title.lchop('#')}") | ||||
|  | ||||
|       video_count_txt = extract_text(item_contents["hashtagVideoCount"]?)     # E.g "203K videos" | ||||
|       channel_count_txt = extract_text(item_contents["hashtagChannelCount"]?) # E.g "81K channels" | ||||
|  | ||||
|       # Fallback for video/channel counts | ||||
|       if channel_count_txt.nil? || video_count_txt.nil? | ||||
|         # E.g: "203K videos • 81K channels" | ||||
|         info_text = extract_text(item_contents["hashtagInfoText"]?).try &.split(" • ") | ||||
|  | ||||
|         if info_text && info_text.size == 2 | ||||
|           video_count_txt ||= info_text[0] | ||||
|           channel_count_txt ||= info_text[1] | ||||
|         end | ||||
|       end | ||||
|  | ||||
|       return SearchHashtag.new({ | ||||
|         title:         title, | ||||
|         url:           url, | ||||
|         video_count:   short_text_to_number(video_count_txt || ""), | ||||
|         channel_count: short_text_to_number(channel_count_txt || ""), | ||||
|       }) | ||||
|     rescue ex | ||||
|       LOGGER.debug("HashtagRendererParser: Failed to extract renderer.") | ||||
|       LOGGER.debug("HashtagRendererParser: Got exception: #{ex.message}") | ||||
|       return nil | ||||
|     end | ||||
|  | ||||
|     def self.parser_name | ||||
|       return {{@type.name}} | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   # Parses a InnerTube gridPlaylistRenderer into a SearchPlaylist. Returns nil when the given object isn't a gridPlaylistRenderer | ||||
|   # | ||||
|   # A gridPlaylistRenderer renders a playlist, that is located in a grid, to click on within the YouTube and Invidious UI. | ||||
|   | ||||
		Reference in New Issue
	
	Block a user