diff --git a/src/invidious/channel/featured_channels.cr b/src/invidious/channel/featured_channels.cr index 73f8c99b..860f25e6 100644 --- a/src/invidious/channel/featured_channels.cr +++ b/src/invidious/channel/featured_channels.cr @@ -1,82 +1,58 @@ -def fetch_channel_featured_channels(ucid, params, view = nil, shelf_id = nil, continuation = nil, query_title = nil) : {Array(Category), (String | Nil)} - # Continuation to load more channel catagories - if continuation.is_a?(String) - initial_data = request_youtube_api_browse(continuation) - items = extract_items(initial_data) - continuation_token = fetch_continuation_token(initial_data) +# Fetches the featured channel categories of a channel +# +# Returned as an array of Category objects containing different channels. +def fetch_channel_featured_channels(ucid) : Array(Category) + initial_data = request_youtube_api_browse(ucid, "EghjaGFubmVscw%3D%3D") - return [Category.new({ - title: query_title.not_nil!, # If continuation contents is requested then the query_title has to be passed along. + channels_tab = extract_selected_tab(initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]) + + # The submenu is the content type menu, and is used to select which categories to view fully. + # As a result, it contains the category title which we'll use as a fallback, since Innertube doesn't + # return the title when the channel only has one featured channel category. + submenu = channels_tab["content"]["sectionListRenderer"]["subMenu"]? + + # If the featured channel tabs lacks categories then that means the channel doesn't feature any other channels. + if !submenu + return [] of Category + end + + # Fetches the fallback title. + submenu_data = submenu["channelSubMenuRenderer"]["contentTypeSubMenuItems"] + fallback_title = submenu_data.as_a.select(&.["selected"].as_bool)[0]["title"].as_s + + items = extract_items(initial_data) + + category_array = [] of Category + items.each do |category| + # The items can either be an Array of Categories or an Array of channels. + if !category.is_a?(Category) + break + end + + # category.title = category.title.empty? ? fallback_title : category.title + category_array << category + end + + # If the returned data is only an array of channels then it means that the featured channel tab only has one category. + # This is due to the fact that InnerTube uses a "gridRenderer" (an array of items) when only one category is present. + # However, as the InnerTube result is a "gridRenderer" and not an "shelfRenderer", an object representing an + # category or section on youtube, we'll lack the category title. But, the good news is that the title of the category is still stored within the submenu + # which we fetched above. We can then use all of these values together to produce a Category object. + if category_array.empty? + category_array << Category.new({ + title: fallback_title, contents: items, description_html: "", url: nil, badges: nil, - })], continuation_token - else - url = nil - if view && shelf_id - url = "/channel/#{ucid}/channels?view=#{view}&shelf_id=#{shelf_id}" - - params = produce_featured_channel_browse_param(view.to_i64, shelf_id.to_i64) - initial_data = request_youtube_api_browse(ucid, params) - continuation_token = fetch_continuation_token(initial_data) - else - initial_data = request_youtube_api_browse(ucid, params) - continuation_token = nil - end - - channels_tab = extract_selected_tab(initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]) - submenu = channels_tab["content"]["sectionListRenderer"]["subMenu"]? - - # There's no submenu data if the channel doesn't feature any channels. - if !submenu - return {[] of Category, continuation_token} - end - - submenu_data = submenu["channelSubMenuRenderer"]["contentTypeSubMenuItems"] - - items = extract_items(initial_data) - fallback_title = submenu_data.as_a.select(&.["selected"].as_bool)[0]["title"].as_s - - # Although extract_items parsed everything into the right structs, we still have - # to fill in the title (if missing) attribute since Youtube doesn't return it when requesting - # a full category (initial) - category_array = [] of Category - items.each do |category| - # Tell compiler that the result from extract_items has to be an array of Categories - if !category.is_a?(Category) - next - end - - category_array << Category.new({ - title: category.title.empty? ? fallback_title : category.title, - contents: category.contents, - description_html: category.description_html, - url: category.url, - badges: nil, - }) - end - - # If no categories has been parsed then it means two things. - # - We're currently viewing a specific channel category - # - We're currently requesting the continuation contents for that specific category. - # And since Youtube dyanmically loads more channels onto the page via JS, the data returned from InnerTube will only contains - # channels. Thus, we'll just go ahead and create one for the template to use. - if category_array.empty? - category_array << Category.new({ - title: fallback_title, - contents: items, - description_html: "", - url: url, - badges: nil, - }) - end - - return category_array, continuation_token + }) end + + return category_array end -def produce_featured_channel_browse_param(view : Int64, shelf_id : Int64) +# Produces the InnerTube parameter for requesting the contents of a specific channel featuring category +private def produce_featured_channel_browse_param(view : Int64, shelf_id : Int64) object = { "2:string" => "channels", "4:varint" => view, @@ -90,3 +66,50 @@ def produce_featured_channel_browse_param(view : Int64, shelf_id : Int64) return browse_params end + +# Fetches the first set of channels from a selected channel featuring category +def fetch_selected_channel_featuring_category(ucid, view, shelf_id) : Tuple(Category, String | Nil) + category_url = "/channel/#{ucid}/channels?view=#{view}&shelf_id=#{shelf_id}" + + params = produce_featured_channel_browse_param(view.to_i64, shelf_id.to_i64) + initial_data = request_youtube_api_browse(ucid, params) + channels_tab = extract_selected_tab(initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]) + continuation_token = fetch_continuation_token(initial_data) + + # Fetches the fallback title + submenu = channels_tab["content"]["sectionListRenderer"]["subMenu"] + submenu_data = submenu["channelSubMenuRenderer"]["contentTypeSubMenuItems"] + fallback_title = submenu_data.as_a.select(&.["selected"].as_bool)[0]["title"].as_s + + items = extract_items(initial_data) + + # Since the returned items from InnerTube is an array of channels, (See explanation at the end of the fetch_channel_featured_channels function) + # we lack the category title attribute. However, it is still stored as a submenu data which we fetched above. We can then use all of these + # values together to produce a Category object. + return Category.new({ + title: fallback_title, + contents: items, + description_html: "", + url: category_url, + badges: nil, + }), continuation_token +end + +# Fetches the next set of channels within the selected channel featuring category. +# Requires the continuation token and the query_title. +# +# TODO: The query_title here is really only used for frontend rendering. +# And since it's a URL parameter we should be able to just request it directly within the template files. +def fetch_channel_featured_channels_category_continuation(continuation, query_title) : Tuple(Category, String | Nil) + initial_data = request_youtube_api_browse(continuation) + items = extract_items(initial_data) + continuation_token = fetch_continuation_token(initial_data) + + return Category.new({ + title: query_title.not_nil!, # If continuation contents is requested then the query_title has to be passed along. + contents: items, + description_html: "", + url: nil, + badges: nil, + }), continuation_token +end diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index 2a9aacc4..373e22ef 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -116,36 +116,33 @@ class Invidious::Routes::Channels < Invidious::Routes::BaseRoute view = env.params.query["view"]? shelf_id = env.params.query["shelf_id"]? + + # The offset is mainly to check if we're at the first page or not and in turn whether we should have a "previous page" button or not. + offset = env.params.query["offset"]? + if offset + offset = offset.to_i + else + offset = 0 + end + # Category title isn't returned when requesting a specific category or continuation data # so we have it in through a url param current_category_title = env.params.query["title"]? + previous_continuation = env.params.query["previous"]? + 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"]? - - featured_channel_categories, continuation_token = fetch_channel_featured_channels(ucid, "EghjaGFubmVscw%3D%3D", nil, nil, continuation, current_category_title).not_nil! + featured_channel_categories, continuation_token = fetch_channel_featured_channels_category_continuation(continuation, current_category_title) elsif view && shelf_id - offset = env.params.query["offset"]? - if offset - offset = offset.to_i - else - offset = 0 - end - - featured_channel_categories, continuation_token = fetch_channel_featured_channels(ucid, "EghjaGFubmVscw%3D%3D", view, shelf_id, continuation, current_category_title).not_nil! + featured_channel_categories, continuation_token = fetch_selected_channel_featuring_category(ucid, view, shelf_id) else - previous_continuation = nil - offset = 0 + continuation_token = nil + featured_channel_categories = fetch_channel_featured_channels(ucid) + end - featured_channel_categories, continuation_token = fetch_channel_featured_channels(ucid, "EghjaGFubmVscw%3D%3D", nil, nil, current_category_title).not_nil! + # If we only got a single category we'll go ahead and wrap it within an array for easier processing in the template. + if featured_channel_categories.is_a? Category + featured_channel_categories = [featured_channel_categories] end templated "channel/featured_channels", buffer_footer: true diff --git a/src/invidious/views/channel/featured_channels.ecr b/src/invidious/views/channel/featured_channels.ecr index 8c0c145f..34ed2f17 100644 --- a/src/invidious/views/channel/featured_channels.ecr +++ b/src/invidious/views/channel/featured_channels.ecr @@ -91,6 +91,7 @@ <% if !featured_channel_categories.empty? %> <% base_url = "/channel/#{channel.ucid}/channels?view=#{view}&shelf_id=#{shelf_id}" %> +