mirror of https://github.com/iv-org/invidious
Add support for the new channel layout - part 2 (#3419)
commit
05258d56bd
@ -1,93 +1,28 @@
|
||||
def fetch_channel_playlists(ucid, author, continuation, sort_by)
|
||||
if continuation
|
||||
response_json = YoutubeAPI.browse(continuation)
|
||||
continuation_items = response_json["onResponseReceivedActions"]?
|
||||
.try &.[0]["appendContinuationItemsAction"]["continuationItems"]
|
||||
|
||||
return [] of SearchItem, nil if !continuation_items
|
||||
|
||||
items = [] of SearchItem
|
||||
continuation_items.as_a.select(&.as_h.has_key?("gridPlaylistRenderer")).each { |item|
|
||||
extract_item(item, author, ucid).try { |t| items << t }
|
||||
}
|
||||
|
||||
continuation = continuation_items.as_a.last["continuationItemRenderer"]?
|
||||
.try &.["continuationEndpoint"]["continuationCommand"]["token"].as_s
|
||||
else
|
||||
url = "/channel/#{ucid}/playlists?flow=list&view=1"
|
||||
|
||||
case sort_by
|
||||
when "last", "last_added"
|
||||
#
|
||||
when "oldest", "oldest_created"
|
||||
url += "&sort=da"
|
||||
when "newest", "newest_created"
|
||||
url += "&sort=dd"
|
||||
else nil # Ignore
|
||||
end
|
||||
|
||||
response = YT_POOL.client &.get(url)
|
||||
initial_data = extract_initial_data(response.body)
|
||||
return [] of SearchItem, nil if !initial_data
|
||||
|
||||
items = extract_items(initial_data, author, ucid)
|
||||
continuation = response.body.match(/"token":"(?<continuation>[^"]+)"/).try &.["continuation"]?
|
||||
end
|
||||
|
||||
return items, continuation
|
||||
end
|
||||
|
||||
# ## NOTE: DEPRECATED
|
||||
# Reason -> Unstable
|
||||
# The Protobuf object must be provided with an id of the last playlist from the current "page"
|
||||
# in order to fetch the next one accurately
|
||||
# (if the id isn't included, entries shift around erratically between pages,
|
||||
# leading to repetitions and skip overs)
|
||||
#
|
||||
# Since it's impossible to produce the appropriate Protobuf without an id being provided by the user,
|
||||
# it's better to stick to continuation tokens provided by the first request and onward
|
||||
def produce_channel_playlists_url(ucid, cursor, sort = "newest", auto_generated = false)
|
||||
object = {
|
||||
"80226972:embedded" => {
|
||||
"2:string" => ucid,
|
||||
"3:base64" => {
|
||||
"2:string" => "playlists",
|
||||
"6:varint" => 2_i64,
|
||||
"7:varint" => 1_i64,
|
||||
"12:varint" => 1_i64,
|
||||
"13:string" => "",
|
||||
"23:varint" => 0_i64,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if cursor
|
||||
cursor = Base64.urlsafe_encode(cursor, false) if !auto_generated
|
||||
object["80226972:embedded"]["3:base64"].as(Hash)["15:string"] = cursor
|
||||
end
|
||||
|
||||
if auto_generated
|
||||
object["80226972:embedded"]["3:base64"].as(Hash)["4:varint"] = 0x32_i64
|
||||
initial_data = YoutubeAPI.browse(continuation)
|
||||
else
|
||||
object["80226972:embedded"]["3:base64"].as(Hash)["4:varint"] = 1_i64
|
||||
case sort
|
||||
when "oldest", "oldest_created"
|
||||
object["80226972:embedded"]["3:base64"].as(Hash)["3:varint"] = 2_i64
|
||||
when "newest", "newest_created"
|
||||
object["80226972:embedded"]["3:base64"].as(Hash)["3:varint"] = 3_i64
|
||||
when "last", "last_added"
|
||||
object["80226972:embedded"]["3:base64"].as(Hash)["3:varint"] = 4_i64
|
||||
else nil # Ignore
|
||||
end
|
||||
params =
|
||||
case sort_by
|
||||
when "last", "last_added"
|
||||
# Equivalent to "&sort=lad"
|
||||
# {"2:string": "playlists", "3:varint": 4, "4:varint": 1, "6:varint": 1}
|
||||
"EglwbGF5bGlzdHMYBCABMAE%3D"
|
||||
when "oldest", "oldest_created"
|
||||
# formerly "&sort=da"
|
||||
# Not available anymore :c or maybe ??
|
||||
# {"2:string": "playlists", "3:varint": 2, "4:varint": 1, "6:varint": 1}
|
||||
"EglwbGF5bGlzdHMYAiABMAE%3D"
|
||||
# {"2:string": "playlists", "3:varint": 1, "4:varint": 1, "6:varint": 1}
|
||||
# "EglwbGF5bGlzdHMYASABMAE%3D"
|
||||
when "newest", "newest_created"
|
||||
# Formerly "&sort=dd"
|
||||
# {"2:string": "playlists", "3:varint": 3, "4:varint": 1, "6:varint": 1}
|
||||
"EglwbGF5bGlzdHMYAyABMAE%3D"
|
||||
end
|
||||
|
||||
initial_data = YoutubeAPI.browse(ucid, params: params || "")
|
||||
end
|
||||
|
||||
object["80226972:embedded"]["3:string"] = Base64.urlsafe_encode(Protodec::Any.from_json(Protodec::Any.cast_json(object["80226972:embedded"]["3:base64"])))
|
||||
object["80226972:embedded"].delete("3:base64")
|
||||
|
||||
continuation = object.try { |i| Protodec::Any.cast_json(i) }
|
||||
.try { |i| Protodec::Any.from_json(i) }
|
||||
.try { |i| Base64.urlsafe_encode(i) }
|
||||
.try { |i| URI.encode_www_form(i) }
|
||||
|
||||
return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"
|
||||
return extract_items(initial_data, author, ucid)
|
||||
end
|
||||
|
@ -0,0 +1,44 @@
|
||||
module Invidious::Frontend::ChannelPage
|
||||
extend self
|
||||
|
||||
enum TabsAvailable
|
||||
Videos
|
||||
Shorts
|
||||
Streams
|
||||
Playlists
|
||||
Community
|
||||
Channels
|
||||
end
|
||||
|
||||
def generate_tabs_links(locale : String, channel : AboutChannel, selected_tab : TabsAvailable)
|
||||
return String.build(1500) do |str|
|
||||
base_url = "/channel/#{channel.ucid}"
|
||||
|
||||
TabsAvailable.each do |tab|
|
||||
# Ignore playlists, as it is not supported for auto-generated channels yet
|
||||
next if (tab.playlists? && channel.auto_generated)
|
||||
|
||||
tab_name = tab.to_s.downcase
|
||||
|
||||
if channel.tabs.includes? tab_name
|
||||
str << %(<div class="pure-u-1 pure-md-1-3">\n)
|
||||
|
||||
if tab == selected_tab
|
||||
str << "\t<b>"
|
||||
str << translate(locale, "channel_tab_#{tab_name}_label")
|
||||
str << "</b>\n"
|
||||
else
|
||||
# Video tab doesn't have the last path component
|
||||
url = tab.videos? ? base_url : "#{base_url}/#{tab_name}"
|
||||
|
||||
str << %(\t<a href=") << url << %(">)
|
||||
str << translate(locale, "channel_tab_#{tab_name}_label")
|
||||
str << "</a>\n"
|
||||
end
|
||||
|
||||
str << "</div>"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,60 @@
|
||||
<% if channel.banner %>
|
||||
<div class="h-box">
|
||||
<img style="width:100%" src="/ggpht<%= URI.parse(channel.banner.not_nil!.gsub("=w1060-", "=w1280-")).request_target %>">
|
||||
</div>
|
||||
|
||||
<div class="h-box">
|
||||
<hr>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="pure-g h-box">
|
||||
<div class="pure-u-2-3">
|
||||
<div class="channel-profile">
|
||||
<img src="/ggpht<%= channel_profile_pic %>">
|
||||
<span><%= author %></span><% if !channel.verified.nil? && channel.verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1-3">
|
||||
<h3 style="text-align:right">
|
||||
<a href="/feed/channel/<%= ucid %>"><i class="icon ion-logo-rss"></i></a>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-box">
|
||||
<div id="descriptionWrapper">
|
||||
<p><span style="white-space:pre-wrap"><%= channel.description_html %></span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-box">
|
||||
<% sub_count_text = number_to_short_text(channel.sub_count) %>
|
||||
<%= rendered "components/subscribe_widget" %>
|
||||
</div>
|
||||
|
||||
<div class="pure-g h-box">
|
||||
<div class="pure-u-1-2">
|
||||
<div class="pure-u-1 pure-md-1-3">
|
||||
<a href="<%= youtube_url %>"><%= translate(locale, "View channel on YouTube") %></a>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-md-1-3">
|
||||
<a href="<%= redirect_url %>"><%= translate(locale, "Switch Invidious Instance") %></a>
|
||||
</div>
|
||||
|
||||
<%= Invidious::Frontend::ChannelPage.generate_tabs_links(locale, channel, selected_tab) %>
|
||||
</div>
|
||||
<div class="pure-u-1-2">
|
||||
<div class="pure-g" style="text-align:end">
|
||||
<% sort_options.each do |sort| %>
|
||||
<div class="pure-u-1 pure-md-1-3">
|
||||
<% if sort_by == sort %>
|
||||
<b><%= translate(locale, sort) %></b>
|
||||
<% else %>
|
||||
<a href="<%= relative_url %>?sort_by=<%= sort %>"><%= translate(locale, sort) %></a>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,108 +0,0 @@
|
||||
<% ucid = channel.ucid %>
|
||||
<% author = HTML.escape(channel.author) %>
|
||||
|
||||
<% content_for "header" do %>
|
||||
<title><%= author %> - Invidious</title>
|
||||
<% end %>
|
||||
|
||||
<% if channel.banner %>
|
||||
<div class="h-box">
|
||||
<img style="width:100%" src="/ggpht<%= URI.parse(channel.banner.not_nil!.gsub("=w1060-", "=w1280-")).request_target %>">
|
||||
</div>
|
||||
|
||||
<div class="h-box">
|
||||
<hr>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="pure-g h-box">
|
||||
<div class="pure-u-2-3">
|
||||
<div class="channel-profile">
|
||||
<img src="/ggpht<%= URI.parse(channel.author_thumbnail).request_target %>">
|
||||
<span><%= author %></span><% if !channel.verified.nil? && channel.verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1-3" style="text-align:right">
|
||||
<h3 style="text-align:right">
|
||||
<a href="/feed/channel/<%= ucid %>"><i class="icon ion-logo-rss"></i></a>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-box">
|
||||
<div id="descriptionWrapper">
|
||||
<p><span style="white-space:pre-wrap"><%= XML.parse_html(channel.description_html).xpath_node(%q(.//pre)).try &.content if !channel.description_html.empty? %></span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-box">
|
||||
<% sub_count_text = number_to_short_text(channel.sub_count) %>
|
||||
<%= rendered "components/subscribe_widget" %>
|
||||
</div>
|
||||
|
||||
<div class="pure-g h-box">
|
||||
<div class="pure-g pure-u-1-3">
|
||||
<div class="pure-u-1 pure-md-1-3">
|
||||
<a href="https://www.youtube.com/channel/<%= ucid %>/playlists"><%= translate(locale, "View channel on YouTube") %></a>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1 pure-md-1-3">
|
||||
<% if env.get("preferences").as(Preferences).automatic_instance_redirect%>
|
||||
<a href="/redirect?referer=<%= env.get?("current_page") %>"><%= translate(locale, "Switch Invidious Instance") %></a>
|
||||
<% else %>
|
||||
<a href="https://redirect.invidious.io<%= env.request.resource %>"><%= translate(locale, "Switch Invidious Instance") %></a>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1 pure-md-1-3">
|
||||
<a href="/channel/<%= ucid %>"><%= translate(locale, "Videos") %></a>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-md-1-3">
|
||||
<% if !channel.auto_generated %>
|
||||
<b><%= translate(locale, "Playlists") %></b>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-md-1-3">
|
||||
<% if channel.tabs.includes? "community" %>
|
||||
<a href="/channel/<%= ucid %>/community"><%= translate(locale, "Community") %></a>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1-3"></div>
|
||||
<div class="pure-u-1-3">
|
||||
<div class="pure-g" style="text-align:right">
|
||||
<% {"last", "oldest", "newest"}.each do |sort| %>
|
||||
<div class="pure-u-1 pure-md-1-3">
|
||||
<% if sort_by == sort %>
|
||||
<b><%= translate(locale, sort) %></b>
|
||||
<% else %>
|
||||
<a href="/channel/<%= ucid %>/playlists?sort_by=<%= sort %>">
|
||||
<%= translate(locale, sort) %>
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h-box">
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<div class="pure-g">
|
||||
<% items.each do |item| %>
|
||||
<%= rendered "components/item" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="pure-g h-box">
|
||||
<div class="pure-u-1 pure-u-md-4-5"></div>
|
||||
<div class="pure-u-1 pure-u-lg-1-5" style="text-align:right">
|
||||
<% if continuation %>
|
||||
<a href="/channel/<%= ucid %>/playlists?continuation=<%= continuation %><% if sort_by != "last" %>&sort_by=<%= URI.encode_www_form(sort_by) %><% end %>">
|
||||
<%= translate(locale, "Next page") %>
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
Loading…
Reference in New Issue