mirror of
				https://gitea.invidious.io/iv-org/invidious-copy-2023-06-08.git
				synced 2024-08-15 00:53:38 +00:00 
			
		
		
		
	Add support for featured channels page
This commit is contained in:
		
							parent
							
								
									a777eda66a
								
							
						
					
					
						commit
						e9dcac9bd4
					
				
					 46 changed files with 543 additions and 48 deletions
				
			
		|  | @ -50,3 +50,70 @@ | |||
| #link-widget-primary a:hover { | ||||
|   color: #e1e1e1 !important; | ||||
| } | ||||
| 
 | ||||
| /* Featured channels page */ | ||||
| 
 | ||||
| .channel-section details { | ||||
|   margin: 20px; | ||||
| } | ||||
| 
 | ||||
| .channel-section details summary { | ||||
|   margin-bottom: 20px; | ||||
| } | ||||
| 
 | ||||
| .channel-section details summary::marker { | ||||
|   font-size: 1.3em; | ||||
| } | ||||
| 
 | ||||
| .category-heading { | ||||
|   font-size: 1.2em; | ||||
|   user-select: none; | ||||
|   display: inline; | ||||
| } | ||||
| 
 | ||||
| .section-contents .channel-profile { | ||||
|   text-align: center; | ||||
| } | ||||
| 
 | ||||
| /* Due to space constraints we'll make the special large featured channel display | ||||
| only show up when the screen is wide enough */ | ||||
| 
 | ||||
| @media screen and (min-width: 600px) { | ||||
|   .large-featured-channel.channel-profile { | ||||
|     /* We don't want the following attribute for large featured channels*/ | ||||
|     text-align: initial; | ||||
|     margin: initial; | ||||
| 
 | ||||
|     display: flex; | ||||
|   } | ||||
| 
 | ||||
|   .large-featured-channel.channel-profile img { | ||||
|     margin: 20% 0; | ||||
|   } | ||||
| 
 | ||||
|   .large-featured-channel .featured-channel-about { | ||||
|     margin-left: 2em; | ||||
|   } | ||||
| 
 | ||||
|   .large-featured-channel .featured-channel-title { | ||||
|     font-size: 1.2em; | ||||
|     margin-bottom: 10px | ||||
|   } | ||||
| 
 | ||||
|   .large-featured-channel .featured-channel-description { | ||||
|     margin-top: 10px; | ||||
|     font-weight: normal; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /* Replicate the look for a normal featured channel */ | ||||
| @media screen and (max-width: 600px) { | ||||
|   .large-featured-channel .seperator, .large-featured-channel .featured-channel-description { | ||||
|     display: none | ||||
|   } | ||||
| 
 | ||||
|   .large-featured-channel .featured-channel-metadata:last-child { | ||||
|     display: block; | ||||
|     margin-top: 1em; | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -344,7 +344,7 @@ span > select { | |||
| 
 | ||||
| .light-theme a:hover, | ||||
| .light-theme a:active, | ||||
| .light-theme summary:hover { | ||||
| .light-theme .simulated_a:hover { | ||||
|   color: #075A9E !important; | ||||
| } | ||||
| 
 | ||||
|  | @ -371,7 +371,7 @@ span > select { | |||
| @media (prefers-color-scheme: light) { | ||||
|   .no-theme a:hover, | ||||
|   .no-theme a:active, | ||||
|   .no-theme summary:hover  { | ||||
|   .no-theme .simulated_a:hover  { | ||||
|     color: #075A9E !important; | ||||
|   } | ||||
| 
 | ||||
|  | @ -402,7 +402,7 @@ span > select { | |||
| 
 | ||||
| .dark-theme a:hover, | ||||
| .dark-theme a:active, | ||||
| .dark-theme summary:hover { | ||||
| .dark-theme .simulated_a:hover { | ||||
|   color: rgb(0, 182, 240); | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -367,5 +367,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -428,5 +428,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
| } | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -428,5 +428,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "Country: ", | ||||
|     "Stats": "Stats", | ||||
|     "Joined": "Joined", | ||||
|     "Links": "Links" | ||||
|     "Links": "Links", | ||||
|     "This channel doesn't feature any other channels.": "This channel doesn't feature any other channels." | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -350,5 +350,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -349,5 +349,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -428,5 +428,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -367,5 +367,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -426,5 +426,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -350,5 +350,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -429,5 +429,6 @@ | |||
|     "Country: ": "", | ||||
|     "Stats": "", | ||||
|     "Joined": "", | ||||
|     "Links": "" | ||||
|     "Links": "", | ||||
|     "This channel doesn't feature any other channels.": "" | ||||
| } | ||||
|  |  | |||
|  | @ -312,6 +312,8 @@ Invidious::Routing.get "/channel/:ucid", Invidious::Routes::Channels, :home | |||
| Invidious::Routing.get "/channel/:ucid/videos", Invidious::Routes::Channels, :videos | ||||
| Invidious::Routing.get "/channel/:ucid/playlists", Invidious::Routes::Channels, :playlists | ||||
| Invidious::Routing.get "/channel/:ucid/community", Invidious::Routes::Channels, :community | ||||
| Invidious::Routing.get "/channel/:ucid/channels", Invidious::Routes::Channels, :channels | ||||
| Invidious::Routing.get "/channel/:ucid/channels/:param", Invidious::Routes::Channels, :featured_channel_category | ||||
| Invidious::Routing.get "/channel/:ucid/about", Invidious::Routes::Channels, :about | ||||
| 
 | ||||
| Invidious::Routing.get "/watch", Invidious::Routes::Watch, :handle | ||||
|  |  | |||
|  | @ -135,7 +135,7 @@ struct AboutChannel | |||
|   property is_family_friendly : Bool | ||||
|   property allowed_regions : Array(String) | ||||
|   property related_channels : Array(AboutRelatedChannel) | ||||
|   property tabs : Hash(String, String) | ||||
|   property tabs : Hash(String, Tuple(Int32, String)) # TabName => {TabiZZndex, browseEndpoint params} | ||||
|   property links : Array(Tuple(String, String, String)) | ||||
| end | ||||
| 
 | ||||
|  | @ -380,6 +380,27 @@ def fetch_channel_playlists(ucid, author, continuation, sort_by) | |||
|   return items, continuation | ||||
| end | ||||
| 
 | ||||
| def fetch_channel_featured_channels(ucid, tab_data, params = nil, continuation = nil, title = nil ) | ||||
|   if continuation.is_a?(String) | ||||
|      initial_data = request_youtube_api_browse(continuation) | ||||
|      channels_tab_content = initial_data["onResponseReceivedActions"][0]["appendContinuationItemsAction"]["continuationItems"] | ||||
| 
 | ||||
|      return process_featured_channels([channels_tab_content,], nil, title, continuation_items=true) | ||||
|   else | ||||
|     if params.is_a?(String) | ||||
|       initial_data = request_youtube_api_browse(ucid, params) | ||||
|     else | ||||
|       initial_data = request_youtube_api_browse(ucid, tab_data[1]) | ||||
|     end | ||||
| 
 | ||||
|     channels_tab = initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"][tab_data[0]]["tabRenderer"] | ||||
|     channels_tab_content = channels_tab["content"]["sectionListRenderer"]["contents"].as_a | ||||
|     submenu_data = channels_tab["content"]["sectionListRenderer"]["subMenu"]?.try &.["channelSubMenuRenderer"]["contentTypeSubMenuItems"] || false | ||||
| 
 | ||||
|     return process_featured_channels(channels_tab_content, submenu_data) | ||||
|   end | ||||
| end | ||||
| 
 | ||||
| def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false) | ||||
|   object = { | ||||
|     "80226972:embedded" => { | ||||
|  | @ -887,13 +908,16 @@ def get_about_info(ucid, locale) | |||
|   country = "" | ||||
|   total_views = 0_i64 | ||||
|   joined = Time.unix(0) | ||||
|   tabs = {} of String => String # TabName => browseEndpoint params | ||||
|   tabs = {} of String => Tuple(Int32, String)  # TabName => {TabiZZndex, browseEndpoint params} | ||||
|   links = [] of {String, String, String} | ||||
| 
 | ||||
|   tabs_json = initdata["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]?.try &.as_a? | ||||
|   tab_names = [] of String | ||||
|   tab_data = [] of Tuple(Int32, String) | ||||
| 
 | ||||
|   if !tabs_json.nil? | ||||
|     # Retrieve information from the tabs array. The index we are looking for varies between channels. | ||||
|     tabs_json.each do |node| | ||||
|     tabs_json.each_with_index do |node, i| | ||||
|       # Try to find the about section which is located in only one of the tabs. | ||||
|       channel_about_meta = node["tabRenderer"]?.try &.["content"]?.try &.["sectionListRenderer"]? | ||||
|         .try &.["contents"]?.try &.[0]?.try &.["itemSectionRenderer"]?.try &.["contents"]? | ||||
|  | @ -935,10 +959,14 @@ def get_about_info(ucid, locale) | |||
|           auto_generated = true | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       if node["tabRenderer"]? | ||||
|         tab_names << node["tabRenderer"]["title"].as_s.downcase | ||||
|         tab_data << {i, node["tabRenderer"]["endpoint"]["browseEndpoint"]["params"].as_s} | ||||
|       end | ||||
| 
 | ||||
|     end | ||||
|     tab_names = tabs_json.reject { |node| node["tabRenderer"]?.nil? }.map { |node| node["tabRenderer"]["title"].as_s.downcase } | ||||
|     browse_endpoint_param = tabs_json.reject { |node| node["tabRenderer"]?.nil? }.map { |node| node["tabRenderer"]["endpoint"]["browseEndpoint"]["params"].as_s } | ||||
|     tabs = Hash.zip(tab_names, browse_endpoint_param) | ||||
|     tabs = Hash.zip(tab_names, tab_data) | ||||
|   end | ||||
| 
 | ||||
|   sub_count = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["subscriberCountText"]?.try &.["simpleText"]?.try &.as_s? | ||||
|  |  | |||
							
								
								
									
										170
									
								
								src/invidious/featured_channels.cr
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								src/invidious/featured_channels.cr
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,170 @@ | |||
| struct FeaturedChannel | ||||
|   include DB::Serializable | ||||
| 
 | ||||
|   property author : String | ||||
|   property ucid : String | ||||
|   property author_thumbnail : String | ||||
|   property subscriber_count : Int32 | ||||
|   property video_count : Int32 | ||||
|   property description_html : String? | ||||
| 
 | ||||
|   def to_json(locale, json : JSON::Builder) | ||||
|     json.object do | ||||
|       json.field "author", self.author | ||||
|       json.field "authorId", self.ucid | ||||
|       json.field "authorUrl", "/channel/#{self.ucid}" | ||||
|       json.field "authorThumbnails" do | ||||
|         json.array do | ||||
|           qualities = {32, 48, 76, 100, 176, 512} | ||||
| 
 | ||||
|           qualities.each do |quality| | ||||
|             json.object do | ||||
|               json.field "url", self.author_thumbnail.gsub(/=\d+/, "=s#{quality}") | ||||
|               json.field "width", quality | ||||
|               json.field "height", quality | ||||
|             end | ||||
|           end | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       json.field "description", html_to_content(self.description_html) | ||||
|       json.field "descriptionHtml", self.description_html | ||||
|       json.field "subCount", self.subscriber_count | ||||
|       json.field "videoCount", self.video_count | ||||
|       json.field "badges", self.badges | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def to_json(locale, json : JSON::Builder | Nil = nil) | ||||
|     if json | ||||
|       to_json(locale, json) | ||||
|     else | ||||
|       JSON.build do |json| | ||||
|         to_json(locale, json) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
| 
 | ||||
| struct Category | ||||
|   include DB::Serializable | ||||
| 
 | ||||
|   property title : String | ||||
|   property contents : Array(FeaturedChannel) | FeaturedChannel | ||||
|   property browse_endpoint_param : String? | ||||
|   property continuation_token : String? | ||||
| 
 | ||||
|   def to_json(locale, json : JSON::Builder) | ||||
|     json.object do | ||||
|       json.field "title", self.title | ||||
|       json.field "contents", self.contents | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def to_json(locale, json : JSON::Builder | Nil = nil) | ||||
|     if json | ||||
|       to_json(locale, json) | ||||
|     else | ||||
|       JSON.build do |json| | ||||
|         to_json(locale, json) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
| 
 | ||||
| def _extract_channel_data(channel) | ||||
|   ucid = channel["channelId"].as_s | ||||
|   author = channel["title"]["simpleText"].as_s | ||||
|   author_thumbnail = channel["thumbnail"]["thumbnails"].as_a[0]["url"].as_s | ||||
|   subscriber_count = channel["subscriberCountText"]?.try &.["simpleText"]?.try &.as_s? | ||||
|     .try { |text| short_text_to_number(text.split(" ")[0]) } || 0 | ||||
| 
 | ||||
|   video_count = channel["videoCountText"]?.try &.["runs"][0]["text"].as_s.gsub(/\D/, "").to_i || 0 | ||||
| 
 | ||||
|   if channel["descriptionSnippet"]? | ||||
|     description = channel["descriptionSnippet"]["runs"][0]["text"].as_s | ||||
|     description_html = HTML.escape(description).gsub("\n", "") | ||||
|   else | ||||
|     description_html = nil | ||||
|   end | ||||
| 
 | ||||
|   FeaturedChannel.new({ | ||||
|     author: author, | ||||
|     ucid: ucid, | ||||
|     author_thumbnail: author_thumbnail, | ||||
|     subscriber_count: subscriber_count, | ||||
|     video_count: video_count, | ||||
|     description_html: description_html | ||||
|   }) | ||||
| end | ||||
| 
 | ||||
| def process_featured_channels(data, submenu_data, title=nil, continuation_items=false) | ||||
|   all_categories = [] of Category | ||||
| 
 | ||||
|   if submenu_data.is_a?(Bool) | ||||
|     return all_categories | ||||
|   end | ||||
| 
 | ||||
|   # Extraction process differs when there's more than one category | ||||
|   if data.size > 1 | ||||
|     data.each do |raw_category| | ||||
|       raw_category = raw_category["itemSectionRenderer"]["contents"].as_a[0]["shelfRenderer"] | ||||
| 
 | ||||
|       category_title = raw_category["title"]["runs"][0]["text"].as_s | ||||
|       browse_endpoint_param = raw_category["endpoint"]["browseEndpoint"]["params"].as_s | ||||
| 
 | ||||
|       # Category has multiple channels | ||||
|       if raw_category["content"].as_h.has_key?("horizontalListRenderer") | ||||
|         contents = [] of FeaturedChannel | ||||
|         raw_category["content"]["horizontalListRenderer"]["items"].as_a.each do |channel| | ||||
|           contents << _extract_channel_data(channel["gridChannelRenderer"]) | ||||
|         end | ||||
|       # Single channel | ||||
|       else | ||||
|         channel = raw_category["content"]["expandedShelfContentsRenderer"]["items"][0]["channelRenderer"] | ||||
|         contents = _extract_channel_data(channel) | ||||
|       end | ||||
| 
 | ||||
|       all_categories << Category.new({ | ||||
|         title: category_title, | ||||
|         contents: contents, | ||||
|         browse_endpoint_param: browse_endpoint_param, | ||||
|         continuation_token: nil | ||||
|       }) | ||||
|     end | ||||
|   else | ||||
|     if !continuation_items | ||||
|       raw_category_contents = data[0]["itemSectionRenderer"]["contents"].as_a[0]["gridRenderer"]["items"].as_a | ||||
|     else | ||||
|       raw_category_contents = data[0].as_a | ||||
|     end | ||||
| 
 | ||||
|     category_title = submenu_data.try &.[0]["title"].as_s || title || "" | ||||
| 
 | ||||
|     browse_endpoint_param = nil # Not needed | ||||
|     continuation_token = nil | ||||
| 
 | ||||
|     # If a continuation token is needed it'll always be after at least twelve channels | ||||
|     if raw_category_contents.size > 12 | ||||
|       continuation_token = raw_category_contents[-1]["continuationItemRenderer"]?.try &.["continuationEndpoint"]["continuationCommand"]["token"].as_s || nil | ||||
| 
 | ||||
|       if !continuation_token.nil? | ||||
|         raw_category_contents = raw_category_contents[0..-2] | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     contents = [] of FeaturedChannel | ||||
|     raw_category_contents.each do |channel| | ||||
|       contents << _extract_channel_data(channel["gridChannelRenderer"]) | ||||
|     end | ||||
| 
 | ||||
|     all_categories << Category.new({ | ||||
|       title: category_title, | ||||
|       contents: contents, | ||||
|       browse_endpoint_param: browse_endpoint_param, | ||||
|       continuation_token: continuation_token | ||||
|     }) | ||||
|   end | ||||
| 
 | ||||
|   return all_categories | ||||
| end | ||||
|  | @ -91,6 +91,69 @@ class Invidious::Routes::Channels < Invidious::Routes::BaseRoute | |||
|     templated "community" | ||||
|   end | ||||
| 
 | ||||
|   def channels(env) | ||||
|     data = self.fetch_basic_information(env) | ||||
|     if !data.is_a?(Tuple) | ||||
|       return data | ||||
|     end | ||||
|     locale, user, subscriptions, continuation, ucid, channel = data | ||||
| 
 | ||||
|     if !channel.tabs.has_key?("channels") | ||||
|       return env.redirect "/channel/#{channel.ucid}" | ||||
|     end | ||||
| 
 | ||||
|     # When a channel only has a single category it lacks the category param option so we'll handle it here. | ||||
|     if continuation | ||||
|       offset = env.params.query["offset"]? | ||||
|       if offset | ||||
|         offset = offset.to_i | ||||
|       else | ||||
|         offset = 0 | ||||
|       end | ||||
| 
 | ||||
|       # Previous continuation | ||||
|       previous_continuation = env.params.query["previous"]? | ||||
|       # Category title is not returned when using a continuation token. | ||||
|       title = env.params.query["title"]? | ||||
| 
 | ||||
|       featured_channel_categories = fetch_channel_featured_channels(ucid, channel.tabs["channels"], nil, continuation, title).not_nil! | ||||
|     else | ||||
|       previous_continuation = nil | ||||
|       category_param = nil | ||||
|       offset = 0 | ||||
|       title = nil | ||||
| 
 | ||||
|       featured_channel_categories = fetch_channel_featured_channels(ucid, channel.tabs["channels"], nil, nil).not_nil! | ||||
|     end | ||||
| 
 | ||||
|     templated "channels" | ||||
|   end | ||||
| 
 | ||||
|   def featured_channel_category(env) | ||||
|     # Used to check when the initial page is reached and redirect to /channel/:ucid/channels/:param when zero | ||||
|     offset = env.params.query["offset"]? | ||||
|     category_param = env.params.url["param"] | ||||
|     if offset | ||||
|       offset = offset.to_i | ||||
|     else | ||||
|       offset = 0 | ||||
|     end | ||||
| 
 | ||||
|     data = self.fetch_basic_information(env) | ||||
|     if !data.is_a?(Tuple) | ||||
|       return data | ||||
|     end | ||||
|     locale, user, subscriptions, continuation, ucid, channel = data | ||||
| 
 | ||||
|     # Previous continuation | ||||
|     previous_continuation = env.params.query["previous"]? | ||||
|     # Category title is not returned when using a continuation token. | ||||
|     title = env.params.query["title"]? | ||||
| 
 | ||||
|     featured_channel_categories = fetch_channel_featured_channels(ucid, channel.tabs["channels"], category_param, continuation, title).not_nil! | ||||
|     templated "channels" | ||||
|   end | ||||
| 
 | ||||
|   def about(env) | ||||
|     data = self.fetch_basic_information(env) | ||||
|     if !data.is_a?(Tuple) | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ | |||
| <link rel="stylesheet" href="/css/channel.css?v=<%= ASSET_COMMIT %>"> | ||||
| <% end %> | ||||
| 
 | ||||
| <% content_type =  4 %> | ||||
| <% content_type =  5 %> | ||||
| <% sort_options = Tuple.new %> | ||||
| <%= rendered "components/channel-information" %> | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										115
									
								
								src/invidious/views/channels.ecr
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								src/invidious/views/channels.ecr
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,115 @@ | |||
| <% content_for "header" do %> | ||||
| <title><%= channel.author %> - Invidious</title> | ||||
| <link rel="stylesheet" href="/css/channel.css?v=<%= ASSET_COMMIT %>"> | ||||
| <% end %> | ||||
| 
 | ||||
| <% content_type =  4 %> | ||||
| <% sort_options = Tuple.new %> | ||||
| <%= rendered "components/channel-information" %> | ||||
| 
 | ||||
| <div class="pure-g h-box"> | ||||
|     <% if !featured_channel_categories.empty? %> | ||||
|         <% featured_channel_categories.each do | category | %> | ||||
|             <div class="channel-section pure-u-1"> | ||||
|                 <details open=""> | ||||
|                     <summary style="display: revert;"> | ||||
|                         <h3 class="category-heading"> | ||||
|                             <% if (category_request_param = category.browse_endpoint_param).is_a?(String) %> | ||||
|                                 <a href="/channel/<%=channel.ucid%>/channels/<%=HTML.escape(category_request_param)%>"> | ||||
|                                     <%= category.title %> | ||||
|                                 </a> | ||||
|                             <%else%> | ||||
|                                 <%= category.title %> | ||||
|                             <%end%> | ||||
|                         </h3> | ||||
|                     </summary> | ||||
|                     <% contents = category.contents%> | ||||
|                     <div class="pure-g section-contents"> | ||||
|                         <% if contents.is_a?(Array(FeaturedChannel)) %> | ||||
|                             <% contents.each do |item|%> | ||||
|                                 <div class="channel-profile pure-u-1 pure-u-sm-1-2 pure-u-md-1-3 pure-u-lg-1-4 pure-u-xl-1-5"> | ||||
|                                     <a class="featured-channel-icon" href="/channel/<%= item.ucid %>"> | ||||
|                                         <% if !env.get("preferences").as(Preferences).thin_mode %> | ||||
|                                               <img src="/ggpht<%= URI.parse(item.author_thumbnail).request_target.gsub(/=s\d+/, "=s176") %>"/> | ||||
|                                         <% end %> | ||||
|                                     </a> | ||||
|                                     <div class="featured-channel-about"> | ||||
|                                         <p class="featured-channel-title"><a href="/channel/<%= item.ucid %>"><%= item.author %></a></p> | ||||
|                                         <div class="featured-channel-metadata"> | ||||
|                                             <p><%= translate(locale, "`x` subscribers", number_with_separator(item.subscriber_count)) %></p> | ||||
|                                             <p><%= translate(locale, "`x` videos", number_with_separator(item.video_count)) %></p> | ||||
|                                         </div> | ||||
| 
 | ||||
|                                         <% ucid = item.ucid %> | ||||
|                                         <% author = item.author %> | ||||
|                                         <% sub_count_text = number_to_short_text(item.subscriber_count) %> | ||||
|                                         <%= rendered "components/subscribe_widget" %> | ||||
|                                     </div> | ||||
|                                 </div> | ||||
|                             <%end%> | ||||
|                         <% elsif contents.is_a?(FeaturedChannel) %> | ||||
|                             <%item = contents %> | ||||
|                             <div class="channel-profile large-featured-channel pure-u-1"> | ||||
|                                 <a class="featured-channel-icon" href="/channel/<%= item.ucid %>"> | ||||
|                                     <% if !env.get("preferences").as(Preferences).thin_mode %> | ||||
|                                           <img src="/ggpht<%= URI.parse(item.author_thumbnail).request_target.gsub(/=s\d+/, "=s176") %>"/> | ||||
|                                     <% end %> | ||||
|                                 </a> | ||||
|                                 <div class="featured-channel-about"> | ||||
|                                     <p class="featured-channel-title"><a href="/channel/<%= item.ucid %>"><%= item.author %></a></p> | ||||
|                                     <div class="featured-channel-metadata"> | ||||
|                                         <span><%= translate(locale, "`x` subscribers", number_with_separator(item.subscriber_count)) %></span> | ||||
|                                         <span class="seperator"> | </span> | ||||
|                                         <span><%= translate(locale, "`x` videos", number_with_separator(item.video_count)) %></span> | ||||
|                                     </div> | ||||
|                                     <p class="featured-channel-description"><%= item.description_html %></p> | ||||
| 
 | ||||
|                                     <% ucid = contents.ucid %> | ||||
|                                     <% author = contents.author %> | ||||
|                                     <% sub_count_text = number_to_short_text(contents.subscriber_count) %> | ||||
|                                     <%= rendered "components/subscribe_widget" %> | ||||
|                             </div> | ||||
|                         <%end%> | ||||
|                     </div> | ||||
|                 </details> | ||||
|             </div> | ||||
|         <% end %> | ||||
|     <% else %> | ||||
|         <h3 class="pure-u-1"> | ||||
|             <%= translate(locale, "This channel doesn't feature any other channels.")%> | ||||
|         </h3> | ||||
|     <% end %> | ||||
| 
 | ||||
|     <!-- #<div class="channel-section pure-u-1 pure-u-md-1-4 pure-u-lg-1-6"> --> | ||||
| </div> | ||||
| 
 | ||||
| <div class="pure-g h-box"> | ||||
|     <div class="pure-u-1 pure-u-lg-1-5"> | ||||
|         <% if previous_continuation %> | ||||
|             <a href="/channel/<%=channel.ucid%>/channels/<%=category_param%>?continuation=<%=HTML.escape(previous_continuation)%>&offset=<%=offset.not_nil!-1%>&title=<%=HTML.escape(title.not_nil!)%>"> | ||||
|                 <%= translate(locale, "Previous page") %> | ||||
|             </a> | ||||
|         <% elsif (offset - 1) == 0 %> | ||||
|             <a href="/channel/<%=channel.ucid%>/channels/<%=category_param%>"> | ||||
|                 <%= 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 (next_cont_token = featured_channel_categories[0].continuation_token) %> | ||||
|             <% additional_url_param = ""%> | ||||
|             <% if continuation %> | ||||
|                 <% additional_url_param = "&previous=#{HTML.escape(continuation)}"%> | ||||
|             <%end %> | ||||
|             <% if !title %> | ||||
|                 <% title = featured_channel_categories[0].title %> | ||||
|             <%end %> | ||||
| 
 | ||||
| 
 | ||||
|             <a href="/channel/<%=channel.ucid%>/channels/<%=category_param%>?continuation=<%=HTML.escape(next_cont_token)%>&offset=<%=offset.not_nil!+1%>&title=<%=HTML.escape(title)%><%=additional_url_param%>"> | ||||
|                 <%= translate(locale, "Next page") %> | ||||
|             </a> | ||||
|         <% end %> | ||||
|     </div> | ||||
| </div> | ||||
|  | @ -107,6 +107,20 @@ | |||
|               <% end %> | ||||
| 
 | ||||
|               <% if content_type == 4 %> | ||||
|                   <li class="pure-menu-item pure-menu-selected"> | ||||
|                       <a class="pure-menu-link" href="/channel/<%= channel.ucid %>/channels"> | ||||
|                         <b> <%= translate(locale, "Channels") %> </b> | ||||
|                       </a> | ||||
|                   </li> | ||||
|               <% else %> | ||||
|                   <li class="pure-menu-item"> | ||||
|                       <a class="pure-menu-link" href="/channel/<%= channel.ucid %>/channels"> | ||||
|                         <%= translate(locale, "Channels") %> | ||||
|                       </a> | ||||
|                   </li> | ||||
|               <% end %> | ||||
| 
 | ||||
|               <% if content_type == 5 %> | ||||
|                   <li class="pure-menu-item pure-menu-selected"> | ||||
|                       <a class="pure-menu-link" href="/channel/<%= channel.ucid %>/about"> | ||||
|                         <b> <%= translate(locale, "About") %> </b> | ||||
|  |  | |||
|  | @ -9,7 +9,7 @@ | |||
|     </h3> | ||||
| <% else %> | ||||
|     <details id="filters"> | ||||
|         <summary> | ||||
|         <summary class="simulated_a"> | ||||
|             <h3 style="display:inline"> <%= translate(locale, "filter") %> </h3> | ||||
|         </summary> | ||||
|         <div id="filters" class="pure-g h-box"> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue