Extract channel routes (#2227)
* Extract primary channel routes from invidious.cr Also removes timedtext_video stub since all it does is redirect to the homepage. However, Invidious's 404 handler already does this. -- As the template for the channel about page doesn't exist yet, the behavior for the /channel/:ucid/about endpoint has been changed to be the same as what's currently present on Invidious (cherry picked from commit 8fad19d8057d7d22e3de27ebbc88a9978c1df27b) * Manually extract brand_redirect from 1b569bbc99207cae7c20aa285f42477ae361dd30 This commit manually extracts the brand_redirect function from the commit mentioned. However, the redirect to the `.../about` endpoint is removed due to the fact that it doesn't exist yet. This commit is also mainly just a bridge for the next few cherry picks from \#2215 * Update brand_redirect to use youtubei resolve_url (cherry picked from commit 53335fe7cfdfac392365b7cac447bc7cc6478134) * Add additional channel endpoints to brand_redirect (cherry picked from commit 8fc6f3add637dabb09b2034f4d82fc3d039ba15c) * Add separate handler for /profile endpoint * Add /channel/:ucid/home route * Document all channel brand_urls
This commit is contained in:
parent
4b46313e19
commit
1321c90920
2 changed files with 190 additions and 211 deletions
229
src/invidious.cr
229
src/invidious.cr
|
@ -309,6 +309,24 @@ Invidious::Routing.get "/", Invidious::Routes::Misc, :home
|
||||||
Invidious::Routing.get "/privacy", Invidious::Routes::Misc, :privacy
|
Invidious::Routing.get "/privacy", Invidious::Routes::Misc, :privacy
|
||||||
Invidious::Routing.get "/licenses", Invidious::Routes::Misc, :licenses
|
Invidious::Routing.get "/licenses", Invidious::Routes::Misc, :licenses
|
||||||
|
|
||||||
|
Invidious::Routing.get "/channel/:ucid", Invidious::Routes::Channels, :home
|
||||||
|
Invidious::Routing.get "/channel/:ucid/home", Invidious::Routes::Channels, :home
|
||||||
|
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/community", Invidious::Routes::Channels, :community
|
||||||
|
Invidious::Routing.get "/channel/:ucid/about", Invidious::Routes::Channels, :about
|
||||||
|
|
||||||
|
["", "/videos", "/playlists", "/community", "/about"].each do |path|
|
||||||
|
# /c/LinusTechTips
|
||||||
|
Invidious::Routing.get "/c/:user#{path}", Invidious::Routes::Channels, :brand_redirect
|
||||||
|
# /user/linustechtips | Not always the same as /c/
|
||||||
|
Invidious::Routing.get "/user/:user#{path}", Invidious::Routes::Channels, :brand_redirect
|
||||||
|
# /attribution_link?a=anything&u=/channel/UCZYTClx2T1of7BRZ86-8fow
|
||||||
|
Invidious::Routing.get "/attribution_link#{path}", Invidious::Routes::Channels, :brand_redirect
|
||||||
|
# /profile?user=linustechtips
|
||||||
|
Invidious::Routing.get "/profile/#{path}", Invidious::Routes::Channels, :profile
|
||||||
|
end
|
||||||
|
|
||||||
Invidious::Routing.get "/watch", Invidious::Routes::Watch, :handle
|
Invidious::Routing.get "/watch", Invidious::Routes::Watch, :handle
|
||||||
Invidious::Routing.get "/watch/:id", Invidious::Routes::Watch, :redirect
|
Invidious::Routing.get "/watch/:id", Invidious::Routes::Watch, :redirect
|
||||||
Invidious::Routing.get "/shorts/:id", Invidious::Routes::Watch, :redirect
|
Invidious::Routing.get "/shorts/:id", Invidious::Routes::Watch, :redirect
|
||||||
|
@ -1618,217 +1636,6 @@ end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# YouTube appears to let users set a "brand" URL that
|
|
||||||
# is different from their username, so we convert that here
|
|
||||||
get "/c/:user" do |env|
|
|
||||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
|
||||||
|
|
||||||
user = env.params.url["user"]
|
|
||||||
|
|
||||||
response = YT_POOL.client &.get("/c/#{user}")
|
|
||||||
html = XML.parse_html(response.body)
|
|
||||||
|
|
||||||
ucid = html.xpath_node(%q(//link[@rel="canonical"])).try &.["href"].split("/")[-1]
|
|
||||||
next env.redirect "/" if !ucid
|
|
||||||
|
|
||||||
env.redirect "/channel/#{ucid}"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Legacy endpoint for /user/:username
|
|
||||||
get "/profile" do |env|
|
|
||||||
user = env.params.query["user"]?
|
|
||||||
if !user
|
|
||||||
env.redirect "/"
|
|
||||||
else
|
|
||||||
env.redirect "/user/#{user}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/attribution_link" do |env|
|
|
||||||
if query = env.params.query["u"]?
|
|
||||||
url = URI.parse(query).request_target
|
|
||||||
else
|
|
||||||
url = "/"
|
|
||||||
end
|
|
||||||
|
|
||||||
env.redirect url
|
|
||||||
end
|
|
||||||
|
|
||||||
# Page used by YouTube to provide captioning widget, since we
|
|
||||||
# don't support it we redirect to '/'
|
|
||||||
get "/timedtext_video" do |env|
|
|
||||||
env.redirect "/"
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/user/:user" do |env|
|
|
||||||
user = env.params.url["user"]
|
|
||||||
env.redirect "/channel/#{user}"
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/user/:user/videos" do |env|
|
|
||||||
user = env.params.url["user"]
|
|
||||||
env.redirect "/channel/#{user}/videos"
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/user/:user/about" do |env|
|
|
||||||
user = env.params.url["user"]
|
|
||||||
env.redirect "/channel/#{user}"
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/channel/:ucid/about" do |env|
|
|
||||||
ucid = env.params.url["ucid"]
|
|
||||||
env.redirect "/channel/#{ucid}"
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/channel/:ucid" do |env|
|
|
||||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
|
||||||
|
|
||||||
user = env.get? "user"
|
|
||||||
if user
|
|
||||||
user = user.as(User)
|
|
||||||
subscriptions = user.subscriptions
|
|
||||||
end
|
|
||||||
subscriptions ||= [] of String
|
|
||||||
|
|
||||||
ucid = env.params.url["ucid"]
|
|
||||||
|
|
||||||
page = env.params.query["page"]?.try &.to_i?
|
|
||||||
page ||= 1
|
|
||||||
|
|
||||||
continuation = env.params.query["continuation"]?
|
|
||||||
|
|
||||||
sort_by = env.params.query["sort_by"]?.try &.downcase
|
|
||||||
|
|
||||||
begin
|
|
||||||
channel = get_about_info(ucid, locale)
|
|
||||||
rescue ex : ChannelRedirect
|
|
||||||
next env.redirect env.request.resource.gsub(ucid, ex.channel_id)
|
|
||||||
rescue ex
|
|
||||||
next error_template(500, ex)
|
|
||||||
end
|
|
||||||
|
|
||||||
if channel.auto_generated
|
|
||||||
sort_options = {"last", "oldest", "newest"}
|
|
||||||
sort_by ||= "last"
|
|
||||||
|
|
||||||
items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by)
|
|
||||||
items.uniq! do |item|
|
|
||||||
if item.responds_to?(:title)
|
|
||||||
item.title
|
|
||||||
elsif item.responds_to?(:author)
|
|
||||||
item.author
|
|
||||||
end
|
|
||||||
end
|
|
||||||
items = items.select(&.is_a?(SearchPlaylist)).map(&.as(SearchPlaylist))
|
|
||||||
items.each { |item| item.author = "" }
|
|
||||||
else
|
|
||||||
sort_options = {"newest", "oldest", "popular"}
|
|
||||||
sort_by ||= "newest"
|
|
||||||
|
|
||||||
count, items = get_60_videos(channel.ucid, channel.author, page, channel.auto_generated, sort_by)
|
|
||||||
items.reject! &.paid
|
|
||||||
|
|
||||||
env.set "search", "channel:#{channel.ucid} "
|
|
||||||
end
|
|
||||||
|
|
||||||
templated "channel"
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/channel/:ucid/videos" do |env|
|
|
||||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
|
||||||
|
|
||||||
ucid = env.params.url["ucid"]
|
|
||||||
params = env.request.query
|
|
||||||
|
|
||||||
if !params || params.empty?
|
|
||||||
params = ""
|
|
||||||
else
|
|
||||||
params = "?#{params}"
|
|
||||||
end
|
|
||||||
|
|
||||||
env.redirect "/channel/#{ucid}#{params}"
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/channel/:ucid/playlists" do |env|
|
|
||||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
|
||||||
|
|
||||||
user = env.get? "user"
|
|
||||||
if user
|
|
||||||
user = user.as(User)
|
|
||||||
subscriptions = user.subscriptions
|
|
||||||
end
|
|
||||||
subscriptions ||= [] of String
|
|
||||||
|
|
||||||
ucid = env.params.url["ucid"]
|
|
||||||
|
|
||||||
continuation = env.params.query["continuation"]?
|
|
||||||
|
|
||||||
sort_by = env.params.query["sort_by"]?.try &.downcase
|
|
||||||
sort_by ||= "last"
|
|
||||||
|
|
||||||
begin
|
|
||||||
channel = get_about_info(ucid, locale)
|
|
||||||
rescue ex : ChannelRedirect
|
|
||||||
next env.redirect env.request.resource.gsub(ucid, ex.channel_id)
|
|
||||||
rescue ex
|
|
||||||
next error_template(500, ex)
|
|
||||||
end
|
|
||||||
|
|
||||||
if channel.auto_generated
|
|
||||||
next env.redirect "/channel/#{channel.ucid}"
|
|
||||||
end
|
|
||||||
|
|
||||||
items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by)
|
|
||||||
items = items.select { |item| item.is_a?(SearchPlaylist) }.map { |item| item.as(SearchPlaylist) }
|
|
||||||
items.each { |item| item.author = "" }
|
|
||||||
|
|
||||||
env.set "search", "channel:#{channel.ucid} "
|
|
||||||
templated "playlists"
|
|
||||||
end
|
|
||||||
|
|
||||||
get "/channel/:ucid/community" do |env|
|
|
||||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
|
||||||
|
|
||||||
user = env.get? "user"
|
|
||||||
if user
|
|
||||||
user = user.as(User)
|
|
||||||
subscriptions = user.subscriptions
|
|
||||||
end
|
|
||||||
subscriptions ||= [] of String
|
|
||||||
|
|
||||||
ucid = env.params.url["ucid"]
|
|
||||||
|
|
||||||
thin_mode = env.params.query["thin_mode"]? || env.get("preferences").as(Preferences).thin_mode
|
|
||||||
thin_mode = thin_mode == "true"
|
|
||||||
|
|
||||||
continuation = env.params.query["continuation"]?
|
|
||||||
# sort_by = env.params.query["sort_by"]?.try &.downcase
|
|
||||||
|
|
||||||
begin
|
|
||||||
channel = get_about_info(ucid, locale)
|
|
||||||
rescue ex : ChannelRedirect
|
|
||||||
next env.redirect env.request.resource.gsub(ucid, ex.channel_id)
|
|
||||||
rescue ex
|
|
||||||
next error_template(500, ex)
|
|
||||||
end
|
|
||||||
|
|
||||||
if !channel.tabs.includes? "community"
|
|
||||||
next env.redirect "/channel/#{channel.ucid}"
|
|
||||||
end
|
|
||||||
|
|
||||||
begin
|
|
||||||
items = JSON.parse(fetch_channel_community(ucid, continuation, locale, "json", thin_mode))
|
|
||||||
rescue ex : InfoException
|
|
||||||
env.response.status_code = 500
|
|
||||||
error_message = ex.message
|
|
||||||
rescue ex
|
|
||||||
next error_template(500, ex)
|
|
||||||
end
|
|
||||||
|
|
||||||
env.set "search", "channel:#{channel.ucid} "
|
|
||||||
templated "community"
|
|
||||||
end
|
|
||||||
|
|
||||||
# API Endpoints
|
# API Endpoints
|
||||||
|
|
||||||
get "/api/v1/stats" do |env|
|
get "/api/v1/stats" do |env|
|
||||||
|
|
172
src/invidious/routes/channels.cr
Normal file
172
src/invidious/routes/channels.cr
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
class Invidious::Routes::Channels < Invidious::Routes::BaseRoute
|
||||||
|
def home(env)
|
||||||
|
self.videos(env)
|
||||||
|
end
|
||||||
|
|
||||||
|
def videos(env)
|
||||||
|
data = self.fetch_basic_information(env)
|
||||||
|
if !data.is_a?(Tuple)
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
locale, user, subscriptions, continuation, ucid, channel = data
|
||||||
|
|
||||||
|
page = env.params.query["page"]?.try &.to_i?
|
||||||
|
page ||= 1
|
||||||
|
|
||||||
|
sort_by = env.params.query["sort_by"]?.try &.downcase
|
||||||
|
|
||||||
|
if channel.auto_generated
|
||||||
|
sort_options = {"last", "oldest", "newest"}
|
||||||
|
sort_by ||= "last"
|
||||||
|
|
||||||
|
items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by)
|
||||||
|
items.uniq! do |item|
|
||||||
|
if item.responds_to?(:title)
|
||||||
|
item.title
|
||||||
|
elsif item.responds_to?(:author)
|
||||||
|
item.author
|
||||||
|
end
|
||||||
|
end
|
||||||
|
items = items.select(&.is_a?(SearchPlaylist)).map(&.as(SearchPlaylist))
|
||||||
|
items.each { |item| item.author = "" }
|
||||||
|
else
|
||||||
|
sort_options = {"newest", "oldest", "popular"}
|
||||||
|
sort_by ||= "newest"
|
||||||
|
|
||||||
|
count, items = get_60_videos(channel.ucid, channel.author, page, channel.auto_generated, sort_by)
|
||||||
|
items.reject! &.paid
|
||||||
|
end
|
||||||
|
|
||||||
|
templated "channel"
|
||||||
|
end
|
||||||
|
|
||||||
|
def playlists(env)
|
||||||
|
data = self.fetch_basic_information(env)
|
||||||
|
if !data.is_a?(Tuple)
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
locale, user, subscriptions, continuation, ucid, channel = data
|
||||||
|
|
||||||
|
sort_options = {"last", "oldest", "newest"}
|
||||||
|
sort_by = env.params.query["sort_by"]?.try &.downcase
|
||||||
|
sort_by ||= "last"
|
||||||
|
|
||||||
|
if channel.auto_generated
|
||||||
|
return env.redirect "/channel/#{channel.ucid}"
|
||||||
|
end
|
||||||
|
|
||||||
|
items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by)
|
||||||
|
items = items.select { |item| item.is_a?(SearchPlaylist) }.map { |item| item.as(SearchPlaylist) }
|
||||||
|
items.each { |item| item.author = "" }
|
||||||
|
|
||||||
|
templated "playlists"
|
||||||
|
end
|
||||||
|
|
||||||
|
def community(env)
|
||||||
|
data = self.fetch_basic_information(env)
|
||||||
|
if !data.is_a?(Tuple)
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
locale, user, subscriptions, continuation, ucid, channel = data
|
||||||
|
|
||||||
|
thin_mode = env.params.query["thin_mode"]? || env.get("preferences").as(Preferences).thin_mode
|
||||||
|
thin_mode = thin_mode == "true"
|
||||||
|
|
||||||
|
continuation = env.params.query["continuation"]?
|
||||||
|
# sort_by = env.params.query["sort_by"]?.try &.downcase
|
||||||
|
|
||||||
|
if !channel.tabs.includes? "community"
|
||||||
|
return env.redirect "/channel/#{channel.ucid}"
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
items = JSON.parse(fetch_channel_community(ucid, continuation, locale, "json", thin_mode))
|
||||||
|
rescue ex : InfoException
|
||||||
|
env.response.status_code = 500
|
||||||
|
error_message = ex.message
|
||||||
|
rescue ex
|
||||||
|
return error_template(500, ex)
|
||||||
|
end
|
||||||
|
|
||||||
|
templated "community"
|
||||||
|
end
|
||||||
|
|
||||||
|
def about(env)
|
||||||
|
data = self.fetch_basic_information(env)
|
||||||
|
if !data.is_a?(Tuple)
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
locale, user, subscriptions, continuation, ucid, channel = data
|
||||||
|
|
||||||
|
env.redirect "/channel/#{ucid}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Redirects brand url channels to a normal /channel/:ucid route
|
||||||
|
def brand_redirect(env)
|
||||||
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
|
||||||
|
# /attribution_link endpoint needs both the `a` and `u` parameter
|
||||||
|
# and in order to avoid detection from YouTube we should only send the required ones
|
||||||
|
# without any of the additional url parameters that only Invidious uses.
|
||||||
|
yt_url_params = URI::Params.encode(env.params.query.to_h.select(["a", "u", "user"]))
|
||||||
|
|
||||||
|
# Retrieves URL params that only Invidious uses
|
||||||
|
invidious_url_params = URI::Params.encode(env.params.query.to_h.select!(["a", "u", "user"]))
|
||||||
|
|
||||||
|
begin
|
||||||
|
resolved_url = YoutubeAPI.resolve_url("https://youtube.com#{env.request.path}#{yt_url_params.size > 0 ? "?#{yt_url_params}" : ""}")
|
||||||
|
ucid = resolved_url["endpoint"]["browseEndpoint"]["browseId"]
|
||||||
|
rescue ex : InfoException | KeyError
|
||||||
|
raise InfoException.new(translate(locale, "This channel does not exist."))
|
||||||
|
end
|
||||||
|
|
||||||
|
selected_tab = env.request.path.split("/")[-1]
|
||||||
|
if ["home", "videos", "playlists", "community", "channels", "about"].includes? selected_tab
|
||||||
|
url = "/channel/#{ucid}/#{selected_tab}"
|
||||||
|
else
|
||||||
|
url = "/channel/#{ucid}"
|
||||||
|
end
|
||||||
|
|
||||||
|
env.redirect url
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handles redirects for the /profile endpoint
|
||||||
|
def profile(env)
|
||||||
|
# The /profile endpoint is special. If passed into the resolve_url
|
||||||
|
# endpoint YouTube would return a sign in page instead of an /channel/:ucid
|
||||||
|
# thus we'll add an edge case and handle it here.
|
||||||
|
|
||||||
|
uri_params = env.params.query.size > 0 ? "?#{env.params.query}" : ""
|
||||||
|
|
||||||
|
user = env.params.query["user"]?
|
||||||
|
if !user
|
||||||
|
raise InfoException.new("This channel does not exist.")
|
||||||
|
else
|
||||||
|
env.redirect "/user/#{user}#{uri_params}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def fetch_basic_information(env)
|
||||||
|
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||||
|
|
||||||
|
user = env.get? "user"
|
||||||
|
if user
|
||||||
|
user = user.as(User)
|
||||||
|
subscriptions = user.subscriptions
|
||||||
|
end
|
||||||
|
subscriptions ||= [] of String
|
||||||
|
|
||||||
|
ucid = env.params.url["ucid"]
|
||||||
|
continuation = env.params.query["continuation"]?
|
||||||
|
|
||||||
|
begin
|
||||||
|
channel = get_about_info(ucid, locale)
|
||||||
|
rescue ex : ChannelRedirect
|
||||||
|
return env.redirect env.request.resource.gsub(ucid, ex.channel_id)
|
||||||
|
rescue ex
|
||||||
|
return error_template(500, ex)
|
||||||
|
end
|
||||||
|
|
||||||
|
return {locale, user, subscriptions, continuation, ucid, channel}
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue