Merge 6969e61f5e83138a71d1a2420e83504ccdd702e7 into c88230067b7a3abbb569ac0938bf8d897c8340be

This commit is contained in:
Cameron Radmore 2026-09-18 17:38:41 +00:00 committed by GitHub
commit 81606dae75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 116 additions and 1 deletions

View File

@ -501,6 +501,7 @@
"channel_tab_podcasts_label": "Podcasts",
"channel_tab_releases_label": "Releases",
"channel_tab_courses_label": "Courses",
"channel_tab_shows_label": "Shows",
"channel_tab_playlists_label": "Playlists",
"channel_tab_community_label": "Community",
"channel_tab_posts_label": "Posts",

View File

@ -53,3 +53,12 @@ def fetch_channel_courses(ucid, author, continuation)
end
return extract_items(initial_data, author, ucid)
end
def fetch_channel_shows(ucid, author, continuation)
if continuation
initial_data = YoutubeAPI.browse(continuation)
else
initial_data = YoutubeAPI.browse(ucid, params: "EgVzaG93c_IGBAoCYgA%3D")
end
return extract_items(initial_data, author, ucid)
end

View File

@ -8,6 +8,7 @@ module Invidious::Frontend::ChannelPage
Podcasts
Releases
Courses
Shows
Playlists
Posts
Channels

View File

@ -396,6 +396,35 @@ module Invidious::Routes::API::V1::Channels
end
end
def self.shows(env)
locale = env.get("preferences").as(Preferences).locale
env.response.content_type = "application/json"
ucid = env.params.url["ucid"]
continuation = env.params.query["continuation"]?
# Use the macro defined above
channel = nil # Make the compiler happy
get_channel()
items, next_continuation = fetch_channel_shows(channel.ucid, channel.author, continuation)
JSON.build do |json|
json.object do
json.field "playlists" do
json.array do
items.each do |item|
item.to_json(locale, json) if item.is_a?(SearchPlaylist)
end
end
end
json.field "continuation", next_continuation if next_continuation
end
end
end
def self.community(env)
locale = env.get("preferences").as(Preferences).locale

View File

@ -217,6 +217,30 @@ module Invidious::Routes::Channels
templated "channel"
end
def self.shows(env)
data = self.fetch_basic_information(env)
return data if !data.is_a?(Tuple)
locale, user, subscriptions, continuation, ucid, channel = data
if !channel.tabs.includes? "shows"
return env.redirect "/channel/#{channel.ucid}"
end
sort_by = ""
sort_options = [] of String
items, next_continuation = fetch_channel_shows(
channel.ucid, channel.author, continuation
)
items = items.select(SearchPlaylist)
items.each(&.author = "")
selected_tab = Frontend::ChannelPage::TabsAvailable::Shows
templated "channel"
end
def self.community(env)
return env.redirect env.request.path.sub("posts", "community") if env.request.path.split("/").last == "posts"
@ -331,7 +355,7 @@ module Invidious::Routes::Channels
private KNOWN_TABS = {
"home", "videos", "shorts", "streams", "podcasts",
"releases", "courses", "playlists", "community", "channels", "about",
"releases", "courses", "shows", "playlists", "community", "channels", "about",
"posts",
}

View File

@ -387,6 +387,7 @@ module Invidious::Routes::Playlists
referer = get_referer(env)
plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "")
plid ||= env.params.url["id"]?.try &.[2..]
if !plid
return env.redirect "/"
end

View File

@ -122,6 +122,7 @@ module Invidious::Routing
get "/channel/:ucid/podcasts", Routes::Channels, :podcasts
get "/channel/:ucid/releases", Routes::Channels, :releases
get "/channel/:ucid/courses", Routes::Channels, :courses
get "/channel/:ucid/shows", Routes::Channels, :shows
get "/channel/:ucid/playlists", Routes::Channels, :playlists
get "/channel/:ucid/community", Routes::Channels, :community
get "/channel/:ucid/posts", Routes::Channels, :community
@ -177,6 +178,7 @@ module Invidious::Routing
def register_yt_playlist_routes
get "/playlist", Routes::Playlists, :show
get "/show/:id", Routes::Playlists, :show
get "/mix", Routes::Playlists, :mix
get "/watch_videos", Routes::Playlists, :watch_videos
end
@ -265,6 +267,7 @@ module Invidious::Routing
get "/api/v1/channels/:ucid/podcasts", {{namespace}}::Channels, :podcasts
get "/api/v1/channels/:ucid/releases", {{namespace}}::Channels, :releases
get "/api/v1/channels/:ucid/courses", {{namespace}}::Channels, :courses
get "/api/v1/channels/:ucid/shows", {{namespace}}::Channels, :shows
get "/api/v1/channels/:ucid/playlists", {{namespace}}::Channels, :playlists
get "/api/v1/channels/:ucid/community", {{namespace}}::Channels, :community
get "/api/v1/channels/:ucid/posts", {{namespace}}::Channels, :community

View File

@ -12,6 +12,7 @@
when .podcasts? then "/channel/#{ucid}/podcasts"
when .releases? then "/channel/#{ucid}/releases"
when .courses? then "/channel/#{ucid}/courses"
when .shows? then "/channel/#{ucid}/shows"
else
"/channel/#{ucid}"
end

View File

@ -15,6 +15,7 @@ private ITEM_PARSERS = {
Parsers::VideoRendererParser,
Parsers::ChannelRendererParser,
Parsers::GridPlaylistRendererParser,
Parsers::GridShowRenderer,
Parsers::PlaylistRendererParser,
Parsers::CategoryRendererParser,
Parsers::ReelItemRendererParser,
@ -338,6 +339,50 @@ private module Parsers
end
end
# Parses a InnerTube gridShowRenderer into a SearchPlaylist. Returns nil when the given object isn't a gridShowRenderer
#
# A gridShowRenderer renders a playlist, that is located in a grid, to click on within the YouTube and Invidious UI.
# It is **not** the playlist itself.
#
# See specs for example.
#
# `gridShowRenderer`s can be found on the "shows" tab of channels.
#
module GridShowRenderer
extend self
include BaseParser
def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["gridShowRenderer"]?
return self.parse(item_contents, author_fallback)
end
end
private def parse_internal(item_contents, author_fallback)
title = extract_text(item_contents["title"]) || ""
plid = item_contents.dig?("navigationEndpoint", "commandMetadata", "webCommandMetadata", "url").try &.as_s.gsub("/show/VL", "").split("?sbp")[0] || ""
author_verified = has_verified_badge?(item_contents["ownerBadges"]?)
video_count = item_contents.dig?("thumbnailOverlays", 0, "thumbnailOverlayBottomPanelRenderer", "text", "runs", 0, "text").try(&.as_s.to_i) || 0
playlist_thumbnail = HelperExtractors.get_thumbnails(item_contents.dig("thumbnailRenderer", "showCustomThumbnailRenderer"))
SearchPlaylist.new({
title: title,
id: plid,
author: author_fallback.name,
ucid: author_fallback.id,
video_count: video_count,
videos: [] of SearchPlaylistVideo,
thumbnail: playlist_thumbnail,
author_verified: author_verified,
})
end
def self.parser_name
return {{@type.name}}
end
end
# Parses a InnerTube playlistRenderer into a SearchPlaylist. Returns nil when the given object isn't a playlistRenderer
#
# A playlistRenderer renders a playlist to click on within the YouTube and Invidious UI. It is **not** the playlist itself.
@ -522,6 +567,7 @@ private module Parsers
child ||= ReelItemRendererParser.process(item_contents, author_fallback)
child ||= PlaylistRendererParser.process(item_contents, author_fallback)
child ||= LockupViewModelParser.process(item_contents, author_fallback)
child ||= GridShowRenderer.process(item_contents, author_fallback)
child ||= ShortsLockupViewModelParser.process(item_contents, author_fallback)
return child
end