Add support for featured channels page
This commit is contained in:
parent
a777eda66a
commit
e9dcac9bd4
@ -50,3 +50,70 @@
|
|||||||
#link-widget-primary a:hover {
|
#link-widget-primary a:hover {
|
||||||
color: #e1e1e1 !important;
|
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:hover,
|
||||||
.light-theme a:active,
|
.light-theme a:active,
|
||||||
.light-theme summary:hover {
|
.light-theme .simulated_a:hover {
|
||||||
color: #075A9E !important;
|
color: #075A9E !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,7 +371,7 @@ span > select {
|
|||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
.no-theme a:hover,
|
.no-theme a:hover,
|
||||||
.no-theme a:active,
|
.no-theme a:active,
|
||||||
.no-theme summary:hover {
|
.no-theme .simulated_a:hover {
|
||||||
color: #075A9E !important;
|
color: #075A9E !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -402,7 +402,7 @@ span > select {
|
|||||||
|
|
||||||
.dark-theme a:hover,
|
.dark-theme a:hover,
|
||||||
.dark-theme a:active,
|
.dark-theme a:active,
|
||||||
.dark-theme summary:hover {
|
.dark-theme .simulated_a:hover {
|
||||||
color: rgb(0, 182, 240);
|
color: rgb(0, 182, 240);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -367,5 +367,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -428,5 +428,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
@ -428,5 +428,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "Country: ",
|
"Country: ": "Country: ",
|
||||||
"Stats": "Stats",
|
"Stats": "Stats",
|
||||||
"Joined": "Joined",
|
"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: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -350,5 +350,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -349,5 +349,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -428,5 +428,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -367,5 +367,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -426,5 +426,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -350,5 +350,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"Joined": "",
|
||||||
"Links": ""
|
"Links": "",
|
||||||
|
"This channel doesn't feature any other channels.": ""
|
||||||
}
|
}
|
||||||
|
@ -429,5 +429,6 @@
|
|||||||
"Country: ": "",
|
"Country: ": "",
|
||||||
"Stats": "",
|
"Stats": "",
|
||||||
"Joined": "",
|
"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/videos", Invidious::Routes::Channels, :videos
|
||||||
Invidious::Routing.get "/channel/:ucid/playlists", Invidious::Routes::Channels, :playlists
|
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/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 "/channel/:ucid/about", Invidious::Routes::Channels, :about
|
||||||
|
|
||||||
Invidious::Routing.get "/watch", Invidious::Routes::Watch, :handle
|
Invidious::Routing.get "/watch", Invidious::Routes::Watch, :handle
|
||||||
|
@ -135,7 +135,7 @@ struct AboutChannel
|
|||||||
property is_family_friendly : Bool
|
property is_family_friendly : Bool
|
||||||
property allowed_regions : Array(String)
|
property allowed_regions : Array(String)
|
||||||
property related_channels : Array(AboutRelatedChannel)
|
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))
|
property links : Array(Tuple(String, String, String))
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -380,6 +380,27 @@ def fetch_channel_playlists(ucid, author, continuation, sort_by)
|
|||||||
return items, continuation
|
return items, continuation
|
||||||
end
|
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)
|
def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false)
|
||||||
object = {
|
object = {
|
||||||
"80226972:embedded" => {
|
"80226972:embedded" => {
|
||||||
@ -887,13 +908,16 @@ def get_about_info(ucid, locale)
|
|||||||
country = ""
|
country = ""
|
||||||
total_views = 0_i64
|
total_views = 0_i64
|
||||||
joined = Time.unix(0)
|
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}
|
links = [] of {String, String, String}
|
||||||
|
|
||||||
tabs_json = initdata["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]?.try &.as_a?
|
tabs_json = initdata["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]?.try &.as_a?
|
||||||
|
tab_names = [] of String
|
||||||
|
tab_data = [] of Tuple(Int32, String)
|
||||||
|
|
||||||
if !tabs_json.nil?
|
if !tabs_json.nil?
|
||||||
# Retrieve information from the tabs array. The index we are looking for varies between channels.
|
# 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.
|
# Try to find the about section which is located in only one of the tabs.
|
||||||
channel_about_meta = node["tabRenderer"]?.try &.["content"]?.try &.["sectionListRenderer"]?
|
channel_about_meta = node["tabRenderer"]?.try &.["content"]?.try &.["sectionListRenderer"]?
|
||||||
.try &.["contents"]?.try &.[0]?.try &.["itemSectionRenderer"]?.try &.["contents"]?
|
.try &.["contents"]?.try &.[0]?.try &.["itemSectionRenderer"]?.try &.["contents"]?
|
||||||
@ -935,10 +959,14 @@ def get_about_info(ucid, locale)
|
|||||||
auto_generated = true
|
auto_generated = true
|
||||||
end
|
end
|
||||||
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 }
|
end
|
||||||
tabs = Hash.zip(tab_names, browse_endpoint_param)
|
tabs = Hash.zip(tab_names, tab_data)
|
||||||
end
|
end
|
||||||
|
|
||||||
sub_count = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["subscriberCountText"]?.try &.["simpleText"]?.try &.as_s?
|
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"
|
templated "community"
|
||||||
end
|
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)
|
def about(env)
|
||||||
data = self.fetch_basic_information(env)
|
data = self.fetch_basic_information(env)
|
||||||
if !data.is_a?(Tuple)
|
if !data.is_a?(Tuple)
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<link rel="stylesheet" href="/css/channel.css?v=<%= ASSET_COMMIT %>">
|
<link rel="stylesheet" href="/css/channel.css?v=<%= ASSET_COMMIT %>">
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% content_type = 4 %>
|
<% content_type = 5 %>
|
||||||
<% sort_options = Tuple.new %>
|
<% sort_options = Tuple.new %>
|
||||||
<%= rendered "components/channel-information" %>
|
<%= 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 %>
|
<% end %>
|
||||||
|
|
||||||
<% if content_type == 4 %>
|
<% 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">
|
<li class="pure-menu-item pure-menu-selected">
|
||||||
<a class="pure-menu-link" href="/channel/<%= channel.ucid %>/about">
|
<a class="pure-menu-link" href="/channel/<%= channel.ucid %>/about">
|
||||||
<b> <%= translate(locale, "About") %> </b>
|
<b> <%= translate(locale, "About") %> </b>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
</h3>
|
</h3>
|
||||||
<% else %>
|
<% else %>
|
||||||
<details id="filters">
|
<details id="filters">
|
||||||
<summary>
|
<summary class="simulated_a">
|
||||||
<h3 style="display:inline"> <%= translate(locale, "filter") %> </h3>
|
<h3 style="display:inline"> <%= translate(locale, "filter") %> </h3>
|
||||||
</summary>
|
</summary>
|
||||||
<div id="filters" class="pure-g h-box">
|
<div id="filters" class="pure-g h-box">
|
||||||
|
Loading…
Reference in New Issue
Block a user