Channel: Render age restricted channels

This commit is contained in:
ChunkyProgrammer 2023-11-26 20:24:04 -05:00
parent e319c35f09
commit 96ade642fa
4 changed files with 221 additions and 119 deletions

View File

@ -15,7 +15,8 @@ record AboutChannel,
allowed_regions : Array(String), allowed_regions : Array(String),
tabs : Array(String), tabs : Array(String),
tags : Array(String), tags : Array(String),
verified : Bool verified : Bool,
is_age_gated : Bool
def get_about_info(ucid, locale) : AboutChannel def get_about_info(ucid, locale) : AboutChannel
begin begin
@ -45,46 +46,102 @@ def get_about_info(ucid, locale) : AboutChannel
end end
tags = [] of String tags = [] of String
tab_names = [] of String
total_views = 0_i64
joined = Time.unix(0)
if auto_generated if ageGate = initdata.dig?("contents", "twoColumnBrowseResultsRenderer", "tabs", 0, "tabRenderer", "content", "sectionListRenderer", "contents", 0, "channelAgeGateRenderer")
author = initdata["header"]["interactiveTabbedHeaderRenderer"]["title"]["simpleText"].as_s description_node = nil
author_url = initdata["microformat"]["microformatDataRenderer"]["urlCanonical"].as_s author = ageGate["channelTitle"].as_s
author_thumbnail = initdata["header"]["interactiveTabbedHeaderRenderer"]["boxArt"]["thumbnails"][0]["url"].as_s ucid = initdata["responseContext"]["serviceTrackingParams"][0]["params"][0]["value"].as_s
author_url = "https://www.youtube.com/channel/" + ucid
# Raises a KeyError on failure. author_thumbnail = ageGate["avatar"]["thumbnails"][0]["url"].as_s
banners = initdata["header"]["interactiveTabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]? banner = nil
banner = banners.try &.[-1]?.try &.["url"].as_s? is_family_friendly = false
is_age_gated = true
description_base_node = initdata["header"]["interactiveTabbedHeaderRenderer"]["description"] tab_names = ["videos", "shorts", "streams"]
# some channels have the description in a simpleText auto_generated = false
# ex: https://www.youtube.com/channel/UCQvWX73GQygcwXOTSf_VDVg/
description_node = description_base_node.dig?("simpleText") || description_base_node
tags = initdata.dig?("header", "interactiveTabbedHeaderRenderer", "badges")
.try &.as_a.map(&.["metadataBadgeRenderer"]["label"].as_s) || [] of String
else else
author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s if auto_generated
author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s author = initdata["header"]["interactiveTabbedHeaderRenderer"]["title"]["simpleText"].as_s
author_thumbnail = initdata["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].as_s author_url = initdata["microformat"]["microformatDataRenderer"]["urlCanonical"].as_s
author_verified = has_verified_badge?(initdata.dig?("header", "c4TabbedHeaderRenderer", "badges")) author_thumbnail = initdata["header"]["interactiveTabbedHeaderRenderer"]["boxArt"]["thumbnails"][0]["url"].as_s
ucid = initdata["metadata"]["channelMetadataRenderer"]["externalId"].as_s # Raises a KeyError on failure.
banners = initdata["header"]["interactiveTabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]?
banner = banners.try &.[-1]?.try &.["url"].as_s?
# Raises a KeyError on failure. description_base_node = initdata["header"]["interactiveTabbedHeaderRenderer"]["description"]
banners = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]? # some channels have the description in a simpleText
banners ||= initdata.dig?("header", "pageHeaderRenderer", "content", "pageHeaderViewModel", "banner", "imageBannerViewModel", "image", "sources") # ex: https://www.youtube.com/channel/UCQvWX73GQygcwXOTSf_VDVg/
banner = banners.try &.[-1]?.try &.["url"].as_s? description_node = description_base_node.dig?("simpleText") || description_base_node
# if banner.includes? "channels/c4/default_banner" tags = initdata.dig?("header", "interactiveTabbedHeaderRenderer", "badges")
# banner = nil .try &.as_a.map(&.["metadataBadgeRenderer"]["label"].as_s) || [] of String
# end else
author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s
author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s
author_thumbnail = initdata["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].as_s
author_verified = has_verified_badge?(initdata.dig?("header", "c4TabbedHeaderRenderer", "badges"))
description_node = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]? ucid = initdata["metadata"]["channelMetadataRenderer"]["externalId"].as_s
tags = initdata.dig?("microformat", "microformatDataRenderer", "tags").try &.as_a.map(&.as_s) || [] of String
# Raises a KeyError on failure.
banners = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]?
banners ||= initdata.dig?("header", "pageHeaderRenderer", "content", "pageHeaderViewModel", "banner", "imageBannerViewModel", "image", "sources")
banner = banners.try &.[-1]?.try &.["url"].as_s?
# if banner.includes? "channels/c4/default_banner"
# banner = nil
# end
description_node = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]?
tags = initdata.dig?("microformat", "microformatDataRenderer", "tags").try &.as_a.map(&.as_s) || [] of String
end
is_family_friendly = initdata["microformat"]["microformatDataRenderer"]["familySafe"].as_bool
if tabs_json = initdata["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]?
# Get the name of the tabs available on this channel
tab_names = tabs_json.as_a.compact_map do |entry|
name = entry.dig?("tabRenderer", "title").try &.as_s.downcase
# This is a small fix to not add extra code on the HTML side
# I.e, the URL for the "live" tab is .../streams, so use "streams"
# everywhere for the sake of simplicity
(name == "live") ? "streams" : name
end
# Get the currently active tab ("About")
about_tab = extract_selected_tab(tabs_json)
# Try to find the about metadata section
channel_about_meta = about_tab.dig?(
"content",
"sectionListRenderer", "contents", 0,
"itemSectionRenderer", "contents", 0,
"channelAboutFullMetadataRenderer"
)
if !channel_about_meta.nil?
total_views = channel_about_meta.dig?("viewCountText", "simpleText").try &.as_s.gsub(/\D/, "").to_i64? || 0_i64
# The joined text is split to several sub strings. The reduce joins those strings before parsing the date.
joined = extract_text(channel_about_meta["joinedDateText"]?)
.try { |text| Time.parse(text, "Joined %b %-d, %Y", Time::Location.local) } || Time.unix(0)
# Normal Auto-generated channels
# https://support.google.com/youtube/answer/2579942
# For auto-generated channels, channel_about_meta only has
# ["description"]["simpleText"] and ["primaryLinks"][0]["title"]["simpleText"]
auto_generated = (
(channel_about_meta["primaryLinks"]?.try &.size) == 1 && \
extract_text(channel_about_meta.dig?("primaryLinks", 0, "title")) == "Auto-generated by YouTube" ||
channel_about_meta.dig?("links", 0, "channelExternalLinkViewModel", "title", "content").try &.as_s == "Auto-generated by YouTube"
)
end
end
end end
is_family_friendly = initdata["microformat"]["microformatDataRenderer"]["familySafe"].as_bool
allowed_regions = initdata allowed_regions = initdata
.dig?("microformat", "microformatDataRenderer", "availableCountries") .dig?("microformat", "microformatDataRenderer", "availableCountries")
.try &.as_a.map(&.as_s) || [] of String .try &.as_a.map(&.as_s) || [] of String
@ -102,52 +159,6 @@ def get_about_info(ucid, locale) : AboutChannel
end end
end end
total_views = 0_i64
joined = Time.unix(0)
tab_names = [] of String
if tabs_json = initdata["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]?
# Get the name of the tabs available on this channel
tab_names = tabs_json.as_a.compact_map do |entry|
name = entry.dig?("tabRenderer", "title").try &.as_s.downcase
# This is a small fix to not add extra code on the HTML side
# I.e, the URL for the "live" tab is .../streams, so use "streams"
# everywhere for the sake of simplicity
(name == "live") ? "streams" : name
end
# Get the currently active tab ("About")
about_tab = extract_selected_tab(tabs_json)
# Try to find the about metadata section
channel_about_meta = about_tab.dig?(
"content",
"sectionListRenderer", "contents", 0,
"itemSectionRenderer", "contents", 0,
"channelAboutFullMetadataRenderer"
)
if !channel_about_meta.nil?
total_views = channel_about_meta.dig?("viewCountText", "simpleText").try &.as_s.gsub(/\D/, "").to_i64? || 0_i64
# The joined text is split to several sub strings. The reduce joins those strings before parsing the date.
joined = extract_text(channel_about_meta["joinedDateText"]?)
.try { |text| Time.parse(text, "Joined %b %-d, %Y", Time::Location.local) } || Time.unix(0)
# Normal Auto-generated channels
# https://support.google.com/youtube/answer/2579942
# For auto-generated channels, channel_about_meta only has
# ["description"]["simpleText"] and ["primaryLinks"][0]["title"]["simpleText"]
auto_generated = (
(channel_about_meta["primaryLinks"]?.try &.size) == 1 && \
extract_text(channel_about_meta.dig?("primaryLinks", 0, "title")) == "Auto-generated by YouTube" ||
channel_about_meta.dig?("links", 0, "channelExternalLinkViewModel", "title", "content").try &.as_s == "Auto-generated by YouTube"
)
end
end
sub_count = 0 sub_count = 0
if (metadata_rows = initdata.dig?("header", "pageHeaderRenderer", "content", "pageHeaderViewModel", "metadata", "contentMetadataViewModel", "metadataRows").try &.as_a) if (metadata_rows = initdata.dig?("header", "pageHeaderRenderer", "content", "pageHeaderViewModel", "metadata", "contentMetadataViewModel", "metadataRows").try &.as_a)
@ -177,6 +188,7 @@ def get_about_info(ucid, locale) : AboutChannel
tabs: tab_names, tabs: tab_names,
tags: tags, tags: tags,
verified: author_verified || false, verified: author_verified || false,
is_age_gated: is_age_gated || false,
) )
end end

View File

@ -46,8 +46,14 @@ struct PlaylistVideo
XML.build { |xml| to_xml(xml) } XML.build { |xml| to_xml(xml) }
end end
def to_json(locale : String?, json : JSON::Builder)
to_json(json)
end
def to_json(json : JSON::Builder, index : Int32? = nil) def to_json(json : JSON::Builder, index : Int32? = nil)
json.object do json.object do
json.field "type", "video"
json.field "title", self.title json.field "title", self.title
json.field "videoId", self.id json.field "videoId", self.id
@ -67,6 +73,7 @@ struct PlaylistVideo
end end
json.field "lengthSeconds", self.length_seconds json.field "lengthSeconds", self.length_seconds
json.field "liveNow", self.live_now
end end
end end

View File

@ -27,10 +27,21 @@ module Invidious::Routes::API::V1::Channels
# Retrieve "sort by" setting from URL parameters # Retrieve "sort by" setting from URL parameters
sort_by = env.params.query["sort_by"]?.try &.downcase || "newest" sort_by = env.params.query["sort_by"]?.try &.downcase || "newest"
begin if channel.is_age_gated
videos, _ = Channel::Tabs.get_videos(channel, sort_by: sort_by) begin
rescue ex playlist = get_playlist(channel.ucid.sub("UC", "UULF"))
return error_json(500, ex) videos = get_playlist_videos(playlist, offset: 0)
rescue ex : InfoException
# playlist doesnt exist.
videos = [] of PlaylistVideo
end
next_continuation = nil
else
begin
videos, _ = Channel::Tabs.get_videos(channel, sort_by: sort_by)
rescue ex
return error_json(500, ex)
end
end end
JSON.build do |json| JSON.build do |json|
@ -84,6 +95,7 @@ module Invidious::Routes::API::V1::Channels
json.field "joined", channel.joined.to_unix json.field "joined", channel.joined.to_unix
json.field "autoGenerated", channel.auto_generated json.field "autoGenerated", channel.auto_generated
json.field "ageGated", channel.is_age_gated
json.field "isFamilyFriendly", channel.is_family_friendly json.field "isFamilyFriendly", channel.is_family_friendly
json.field "description", html_to_content(channel.description_html) json.field "description", html_to_content(channel.description_html)
json.field "descriptionHtml", channel.description_html json.field "descriptionHtml", channel.description_html
@ -142,12 +154,23 @@ module Invidious::Routes::API::V1::Channels
sort_by = env.params.query["sort_by"]?.try &.downcase || "newest" sort_by = env.params.query["sort_by"]?.try &.downcase || "newest"
continuation = env.params.query["continuation"]? continuation = env.params.query["continuation"]?
begin if channel.is_age_gated
videos, next_continuation = Channel::Tabs.get_60_videos( begin
channel, continuation: continuation, sort_by: sort_by playlist = get_playlist(channel.ucid.sub("UC", "UULF"))
) videos = get_playlist_videos(playlist, offset: 0)
rescue ex rescue ex : InfoException
return error_json(500, ex) # playlist doesnt exist.
videos = [] of PlaylistVideo
end
next_continuation = nil
else
begin
videos, next_continuation = Channel::Tabs.get_60_videos(
channel, continuation: continuation, sort_by: sort_by
)
rescue ex
return error_json(500, ex)
end
end end
return JSON.build do |json| return JSON.build do |json|
@ -176,12 +199,23 @@ module Invidious::Routes::API::V1::Channels
# Retrieve continuation from URL parameters # Retrieve continuation from URL parameters
continuation = env.params.query["continuation"]? continuation = env.params.query["continuation"]?
begin if channel.is_age_gated
videos, next_continuation = Channel::Tabs.get_shorts( begin
channel, continuation: continuation playlist = get_playlist(channel.ucid.sub("UC", "UUSH"))
) videos = get_playlist_videos(playlist, offset: 0)
rescue ex rescue ex : InfoException
return error_json(500, ex) # playlist doesnt exist.
videos = [] of PlaylistVideo
end
next_continuation = nil
else
begin
videos, next_continuation = Channel::Tabs.get_shorts(
channel, continuation: continuation
)
rescue ex
return error_json(500, ex)
end
end end
return JSON.build do |json| return JSON.build do |json|
@ -211,12 +245,23 @@ module Invidious::Routes::API::V1::Channels
sort_by = env.params.query["sort_by"]?.try &.downcase || "newest" sort_by = env.params.query["sort_by"]?.try &.downcase || "newest"
continuation = env.params.query["continuation"]? continuation = env.params.query["continuation"]?
begin if channel.is_age_gated
videos, next_continuation = Channel::Tabs.get_60_livestreams( begin
channel, continuation: continuation, sort_by: sort_by playlist = get_playlist(channel.ucid.sub("UC", "UULV"))
) videos = get_playlist_videos(playlist, offset: 0)
rescue ex rescue ex : InfoException
return error_json(500, ex) # playlist doesnt exist.
videos = [] of PlaylistVideo
end
next_continuation = nil
else
begin
videos, next_continuation = Channel::Tabs.get_60_livestreams(
channel, continuation: continuation, sort_by: sort_by
)
rescue ex
return error_json(500, ex)
end
end end
return JSON.build do |json| return JSON.build do |json|

View File

@ -36,12 +36,24 @@ module Invidious::Routes::Channels
items = items.select(SearchPlaylist) items = items.select(SearchPlaylist)
items.each(&.author = "") items.each(&.author = "")
else else
sort_options = {"newest", "oldest", "popular"}
# Fetch items and continuation token # Fetch items and continuation token
items, next_continuation = Channel::Tabs.get_videos( if channel.is_age_gated
channel, continuation: continuation, sort_by: (sort_by || "newest") sort_by = ""
) sort_options = [] of String
begin
playlist = get_playlist(channel.ucid.sub("UC", "UULF"))
items = get_playlist_videos(playlist, offset: 0)
rescue ex : InfoException
# playlist doesnt exist.
items = [] of PlaylistVideo
end
next_continuation = nil
else
sort_options = {"newest", "oldest", "popular"}
items, next_continuation = Channel::Tabs.get_videos(
channel, continuation: continuation, sort_by: (sort_by || "newest")
)
end
end end
selected_tab = Frontend::ChannelPage::TabsAvailable::Videos selected_tab = Frontend::ChannelPage::TabsAvailable::Videos
@ -58,14 +70,27 @@ module Invidious::Routes::Channels
return env.redirect "/channel/#{channel.ucid}" return env.redirect "/channel/#{channel.ucid}"
end end
# TODO: support sort option for shorts if channel.is_age_gated
sort_by = "" sort_by = ""
sort_options = [] of String sort_options = [] of String
begin
playlist = get_playlist(channel.ucid.sub("UC", "UUSH"))
items = get_playlist_videos(playlist, offset: 0)
rescue ex : InfoException
# playlist doesnt exist.
items = [] of PlaylistVideo
end
next_continuation = nil
else
# TODO: support sort option for shorts
sort_by = ""
sort_options = [] of String
# Fetch items and continuation token # Fetch items and continuation token
items, next_continuation = Channel::Tabs.get_shorts( items, next_continuation = Channel::Tabs.get_shorts(
channel, continuation: continuation channel, continuation: continuation
) )
end
selected_tab = Frontend::ChannelPage::TabsAvailable::Shorts selected_tab = Frontend::ChannelPage::TabsAvailable::Shorts
templated "channel" templated "channel"
@ -81,13 +106,26 @@ module Invidious::Routes::Channels
return env.redirect "/channel/#{channel.ucid}" return env.redirect "/channel/#{channel.ucid}"
end end
sort_by = env.params.query["sort_by"]?.try &.downcase || "newest" if channel.is_age_gated
sort_options = {"newest", "oldest", "popular"} sort_by = ""
sort_options = [] of String
begin
playlist = get_playlist(channel.ucid.sub("UC", "UULV"))
items = get_playlist_videos(playlist, offset: 0)
rescue ex : InfoException
# playlist doesnt exist.
items = [] of PlaylistVideo
end
next_continuation = nil
else
sort_by = env.params.query["sort_by"]?.try &.downcase || "newest"
sort_options = {"newest", "oldest", "popular"}
# Fetch items and continuation token # Fetch items and continuation token
items, next_continuation = Channel::Tabs.get_60_livestreams( items, next_continuation = Channel::Tabs.get_60_livestreams(
channel, continuation: continuation, sort_by: sort_by channel, continuation: continuation, sort_by: sort_by
) )
end
selected_tab = Frontend::ChannelPage::TabsAvailable::Streams selected_tab = Frontend::ChannelPage::TabsAvailable::Streams
templated "channel" templated "channel"