summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorsyeopite <70992037+syeopite@users.noreply.github.com>2021-08-03 14:46:15 -0700
committerGitHub <noreply@github.com>2021-08-03 23:46:15 +0200
commit1321c9092092a946d66b053f1dc3b71f922053ab (patch)
tree9afb04c026a4fbeda4ec01943213906e9bed6ead /src
parent4b46313e19f06f2739ee522eb4fbfe8dbcbee630 (diff)
downloadinvidious-1321c9092092a946d66b053f1dc3b71f922053ab.tar.gz
invidious-1321c9092092a946d66b053f1dc3b71f922053ab.tar.bz2
invidious-1321c9092092a946d66b053f1dc3b71f922053ab.zip
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
Diffstat (limited to 'src')
-rw-r--r--src/invidious.cr229
-rw-r--r--src/invidious/routes/channels.cr172
2 files changed, 190 insertions, 211 deletions
diff --git a/src/invidious.cr b/src/invidious.cr
index 89292f05..1d183637 100644
--- a/src/invidious.cr
+++ b/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 "/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/:id", Invidious::Routes::Watch, :redirect
Invidious::Routing.get "/shorts/:id", Invidious::Routes::Watch, :redirect
@@ -1618,217 +1636,6 @@ 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
get "/api/v1/stats" do |env|
diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr
new file mode 100644
index 00000000..9876936f
--- /dev/null
+++ b/src/invidious/routes/channels.cr
@@ -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