mirror of
				https://github.com/iv-org/invidious.git
				synced 2025-10-31 12:42:09 +00:00 
			
		
		
		
	Add support for hashtags
This commit is contained in:
		| @@ -385,6 +385,7 @@ end | ||||
|   Invidious::Routing.get "/opensearch.xml", Invidious::Routes::Search, :opensearch | ||||
|   Invidious::Routing.get "/results", Invidious::Routes::Search, :results | ||||
|   Invidious::Routing.get "/search", Invidious::Routes::Search, :search | ||||
|   Invidious::Routing.get "/hashtag/:hashtag", Invidious::Routes::Search, :hashtag | ||||
|  | ||||
|   # User routes | ||||
|   define_user_routes() | ||||
|   | ||||
							
								
								
									
										44
									
								
								src/invidious/hashtag.cr
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/invidious/hashtag.cr
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| module Invidious::Hashtag | ||||
|   extend self | ||||
|  | ||||
|   def fetch(hashtag : String, page : Int, region : String? = nil) : Array(SearchItem) | ||||
|     cursor = (page - 1) * 60 | ||||
|     ctoken = generate_continuation(hashtag, cursor) | ||||
|  | ||||
|     client_config = YoutubeAPI::ClientConfig.new(region: region) | ||||
|     response = YoutubeAPI.browse(continuation: ctoken, client_config: client_config) | ||||
|  | ||||
|     return extract_items(response) | ||||
|   end | ||||
|  | ||||
|   def generate_continuation(hashtag : String, cursor : Int) | ||||
|     object = { | ||||
|       "80226972:embedded" => { | ||||
|         "2:string" => "FEhashtag", | ||||
|         "3:base64" => { | ||||
|           "1:varint" => cursor.to_i64, | ||||
|         }, | ||||
|         "7:base64" => { | ||||
|           "325477796:embedded" => { | ||||
|             "1:embedded" => { | ||||
|               "2:0:embedded" => { | ||||
|                 "2:string"  => '#' + hashtag, | ||||
|                 "4:varint"  => 0_i64, | ||||
|                 "11:string" => "", | ||||
|               }, | ||||
|               "4:string" => "browse-feedFEhashtag", | ||||
|             }, | ||||
|             "2:string" => hashtag, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|     } | ||||
|  | ||||
|     continuation = object.try { |i| Protodec::Any.cast_json(i) } | ||||
|       .try { |i| Protodec::Any.from_json(i) } | ||||
|       .try { |i| Base64.urlsafe_encode(i) } | ||||
|       .try { |i| URI.encode_www_form(i) } | ||||
|  | ||||
|     return continuation | ||||
|   end | ||||
| end | ||||
| @@ -63,4 +63,35 @@ module Invidious::Routes::Search | ||||
|       templated "search" | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def self.hashtag(env : HTTP::Server::Context) | ||||
|     locale = env.get("preferences").as(Preferences).locale | ||||
|  | ||||
|     hashtag = env.params.url["hashtag"]? | ||||
|     if hashtag.nil? || hashtag.empty? | ||||
|       return error_template(400, "Invalid request") | ||||
|     end | ||||
|  | ||||
|     page = env.params.query["page"]? | ||||
|     if page.nil? | ||||
|       page = 1 | ||||
|     else | ||||
|       page = Math.max(1, page.to_i) | ||||
|       env.params.query.delete_all("page") | ||||
|     end | ||||
|  | ||||
|     begin | ||||
|       videos = Invidious::Hashtag.fetch(hashtag, page) | ||||
|     rescue ex | ||||
|       return error_template(500, ex) | ||||
|     end | ||||
|  | ||||
|     params = env.params.query.empty? ? "" : "&#{env.params.query}" | ||||
|  | ||||
|     hashtag_encoded = URI.encode_www_form(hashtag, space_to_plus: false) | ||||
|     url_prev_page = "/hashtag/#{hashtag_encoded}?page=#{page - 1}#{params}" | ||||
|     url_next_page = "/hashtag/#{hashtag_encoded}?page=#{page + 1}#{params}" | ||||
|  | ||||
|     templated "hashtag" | ||||
|   end | ||||
| end | ||||
|   | ||||
							
								
								
									
										39
									
								
								src/invidious/views/hashtag.ecr
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/invidious/views/hashtag.ecr
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| <% content_for "header" do %> | ||||
| <title><%= HTML.escape(hashtag) %> - Invidious</title> | ||||
| <% end %> | ||||
|  | ||||
| <hr/> | ||||
|  | ||||
| <div class="pure-g h-box v-box"> | ||||
|     <div class="pure-u-1 pure-u-lg-1-5"> | ||||
|         <%- if page > 1 -%> | ||||
|             <a href="<%= url_prev_page %>"><%= translate(locale, "Previous page") %></a> | ||||
|         <%- end -%> | ||||
|     </div> | ||||
|     <div class="pure-u-1 pure-u-lg-3-5"></div> | ||||
|     <div class="pure-u-1 pure-u-lg-1-5" style="text-align:right"> | ||||
|         <%- if videos.size >= 60 -%> | ||||
|             <a href="<%= url_next_page %>"><%= translate(locale, "Next page") %></a> | ||||
|         <%- end -%> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| <div class="pure-g"> | ||||
|     <%- videos.each do |item| -%> | ||||
|         <%= rendered "components/item" %> | ||||
|     <%- end -%> | ||||
| </div> | ||||
|  | ||||
| <div class="pure-g h-box"> | ||||
|     <div class="pure-u-1 pure-u-lg-1-5"> | ||||
|         <%- if page > 1 -%> | ||||
|             <a href="<%= url_prev_page %>"><%= translate(locale, "Previous page") %></a> | ||||
|         <%- end -%> | ||||
|     </div> | ||||
|     <div class="pure-u-1 pure-u-lg-3-5"></div> | ||||
|     <div class="pure-u-1 pure-u-lg-1-5" style="text-align:right"> | ||||
|         <%- if videos.size >= 60 -%> | ||||
|             <a href="<%= url_next_page %>"><%= translate(locale, "Next page") %></a> | ||||
|         <%- end -%> | ||||
|     </div> | ||||
| </div> | ||||
| @@ -1,3 +1,5 @@ | ||||
| require "../helpers/serialized_yt_data" | ||||
|  | ||||
| # This file contains helper methods to parse the Youtube API json data into | ||||
| # neat little packages we can use | ||||
|  | ||||
| @@ -14,6 +16,7 @@ private ITEM_PARSERS = { | ||||
|   Parsers::GridPlaylistRendererParser, | ||||
|   Parsers::PlaylistRendererParser, | ||||
|   Parsers::CategoryRendererParser, | ||||
|   Parsers::RichItemRendererParser, | ||||
| } | ||||
|  | ||||
| record AuthorFallback, name : String, id : String | ||||
| @@ -374,6 +377,29 @@ private module Parsers | ||||
|       return {{@type.name}} | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   # Parses an InnerTube richItemRenderer into a SearchVideo. | ||||
|   # Returns nil when the given object isn't a shelfRenderer | ||||
|   # | ||||
|   # A richItemRenderer seems to be a simple wrapper for a videoRenderer, used | ||||
|   # by the result page for hashtags. It is located inside a continuationItems | ||||
|   # container. | ||||
|   # | ||||
|   module RichItemRendererParser | ||||
|     def self.process(item : JSON::Any, author_fallback : AuthorFallback) | ||||
|       if item_contents = item.dig?("richItemRenderer", "content") | ||||
|         return self.parse(item_contents, author_fallback) | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     private def self.parse(item_contents, author_fallback) | ||||
|       return VideoRendererParser.process(item_contents, author_fallback) | ||||
|     end | ||||
|  | ||||
|     def self.parser_name | ||||
|       return {{@type.name}} | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | ||||
| # The following are the extractors for extracting an array of items from | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Samantaz Fox
					Samantaz Fox