From 6c9754e66316d903ed4f89d2cd59cd82940509f5 Mon Sep 17 00:00:00 2001
From: Samantaz Fox <coding@samantaz.fr>
Date: Wed, 30 Nov 2022 00:29:48 +0100
Subject: [PATCH] frontend: Add support for shorts and livestreams

---
 locales/en-US.json                     |  9 +++--
 src/invidious/channels/about.cr        | 10 ++++-
 src/invidious/frontend/channel_page.cr | 43 ++++++++++++++++++++
 src/invidious/routes/channels.cr       | 54 ++++++++++++++++++++++++--
 src/invidious/routing.cr               |  4 +-
 src/invidious/views/channel.ecr        | 30 +++++---------
 src/invidious/views/community.ecr      | 15 +------
 src/invidious/views/playlists.ecr      | 14 +------
 8 files changed, 124 insertions(+), 55 deletions(-)
 create mode 100644 src/invidious/frontend/channel_page.cr

diff --git a/locales/en-US.json b/locales/en-US.json
index 5554b928..44b40c24 100644
--- a/locales/en-US.json
+++ b/locales/en-US.json
@@ -404,9 +404,7 @@
     "`x` marked it with a ❤": "`x` marked it with a ❤",
     "Audio mode": "Audio mode",
     "Video mode": "Video mode",
-    "Videos": "Videos",
     "Playlists": "Playlists",
-    "Community": "Community",
     "search_filters_title": "Filters",
     "search_filters_date_label": "Upload date",
     "search_filters_date_option_none": "Any date",
@@ -472,5 +470,10 @@
     "crash_page_read_the_faq": "read the <a href=\"`x`\">Frequently Asked Questions (FAQ)</a>",
     "crash_page_search_issue": "searched for <a href=\"`x`\">existing issues on GitHub</a>",
     "crash_page_report_issue": "If none of the above helped, please <a href=\"`x`\">open a new issue on GitHub</a> (preferably in English) and include the following text in your message (do NOT translate that text):",
-    "error_video_not_in_playlist": "The requested video doesn't exist in this playlist. <a href=\"`x`\">Click here for the playlist home page.</a>"
+    "error_video_not_in_playlist": "The requested video doesn't exist in this playlist. <a href=\"`x`\">Click here for the playlist home page.</a>",
+    "channel_tab_videos_label": "Videos",
+    "channel_tab_shorts_label": "Shorts",
+    "channel_tab_streams_label": "Livestreams",
+    "channel_tab_playlists_label": "Playlists",
+    "channel_tab_community_label": "Community"
 }
diff --git a/src/invidious/channels/about.cr b/src/invidious/channels/about.cr
index bb9bd8c7..09c3427a 100644
--- a/src/invidious/channels/about.cr
+++ b/src/invidious/channels/about.cr
@@ -104,8 +104,14 @@ def get_about_info(ucid, locale) : AboutChannel
 
   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(&.dig?("tabRenderer", "title").try &.as_s.downcase)
+    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)
diff --git a/src/invidious/frontend/channel_page.cr b/src/invidious/frontend/channel_page.cr
new file mode 100644
index 00000000..7ac0e071
--- /dev/null
+++ b/src/invidious/frontend/channel_page.cr
@@ -0,0 +1,43 @@
+module Invidious::Frontend::ChannelPage
+  extend self
+
+  enum TabsAvailable
+    Videos
+    Shorts
+    Streams
+    Playlists
+    Community
+  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
diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr
index 2773deb7..78b38341 100644
--- a/src/invidious/routes/channels.cr
+++ b/src/invidious/routes/channels.cr
@@ -18,7 +18,7 @@ module Invidious::Routes::Channels
       sort_options = {"last", "oldest", "newest"}
       sort_by ||= "last"
 
-      items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by)
+      items, next_continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by)
       items.uniq! do |item|
         if item.responds_to?(:title)
           item.title
@@ -32,11 +32,59 @@ module Invidious::Routes::Channels
       sort_options = {"newest", "oldest", "popular"}
       sort_by ||= "newest"
 
-      items, continuation = Channel::Tabs.get_60_videos(
+      # Fetch items and continuation token
+      items, next_continuation = Channel::Tabs.get_videos(
         channel, continuation: continuation, sort_by: sort_by
       )
     end
 
+    selected_tab = Frontend::ChannelPage::TabsAvailable::Videos
+    templated "channel"
+  end
+
+  def self.shorts(env)
+    data = self.fetch_basic_information(env)
+    return data if !data.is_a?(Tuple)
+
+    locale, user, subscriptions, continuation, ucid, channel = data
+
+    if !channel.tabs.includes? "shorts"
+      return env.redirect "/channel/#{channel.ucid}"
+    end
+
+    # TODO: support sort option for shorts
+    sort_by = ""
+    sort_options = [] of String
+
+    # Fetch items and continuation token
+    items, next_continuation = Channel::Tabs.get_shorts(
+      channel, continuation: continuation
+    )
+
+    selected_tab = Frontend::ChannelPage::TabsAvailable::Shorts
+    templated "channel"
+  end
+
+  def self.streams(env)
+    data = self.fetch_basic_information(env)
+    return data if !data.is_a?(Tuple)
+
+    locale, user, subscriptions, continuation, ucid, channel = data
+
+    if !channel.tabs.includes? "streams"
+      return env.redirect "/channel/#{channel.ucid}"
+    end
+
+    # TODO: support sort option for livestreams
+    sort_by = ""
+    sort_options = [] of String
+
+    # Fetch items and continuation token
+    items, next_continuation = Channel::Tabs.get_60_livestreams(
+      channel, continuation: continuation
+    )
+
+    selected_tab = Frontend::ChannelPage::TabsAvailable::Streams
     templated "channel"
   end
 
@@ -124,7 +172,7 @@ module Invidious::Routes::Channels
     end
 
     selected_tab = env.request.path.split("/")[-1]
-    if ["home", "videos", "playlists", "community", "channels", "about"].includes? selected_tab
+    if {"home", "videos", "shorts", "streams", "playlists", "community", "channels", "about"}.includes? selected_tab
       url = "/channel/#{ucid}/#{selected_tab}"
     else
       url = "/channel/#{ucid}"
diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr
index f409f13c..08739c3d 100644
--- a/src/invidious/routing.cr
+++ b/src/invidious/routing.cr
@@ -115,6 +115,8 @@ module Invidious::Routing
     get "/channel/:ucid", Routes::Channels, :home
     get "/channel/:ucid/home", Routes::Channels, :home
     get "/channel/:ucid/videos", Routes::Channels, :videos
+    get "/channel/:ucid/shorts", Routes::Channels, :shorts
+    get "/channel/:ucid/streams", Routes::Channels, :streams
     get "/channel/:ucid/playlists", Routes::Channels, :playlists
     get "/channel/:ucid/community", Routes::Channels, :community
     get "/channel/:ucid/about", Routes::Channels, :about
@@ -122,7 +124,7 @@ module Invidious::Routing
     get "/user/:user/live", Routes::Channels, :live
     get "/c/:user/live", Routes::Channels, :live
 
-    ["", "/videos", "/playlists", "/community", "/about"].each do |path|
+    {"", "/videos", "/shorts", "/streams", "/playlists", "/community", "/about"}.each do |path|
       # /c/LinusTechTips
       get "/c/:user#{path}", Routes::Channels, :brand_redirect
       # /user/linustechtips | Not always the same as /c/
diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr
index 878587d4..f6cc3340 100644
--- a/src/invidious/views/channel.ecr
+++ b/src/invidious/views/channel.ecr
@@ -64,23 +64,8 @@
                 <a href="https://redirect.invidious.io<%= env.request.path %>"><%= translate(locale, "Switch Invidious Instance") %></a>
             <% end %>
         </div>
-        <% if !channel.auto_generated %>
-            <div class="pure-u-1 pure-md-1-3">
-                <b><%= translate(locale, "Videos") %></b>
-            </div>
-        <% end %>
-        <div class="pure-u-1 pure-md-1-3">
-            <% if channel.auto_generated %>
-                <b><%= translate(locale, "Playlists") %></b>
-            <% else %>
-                <a href="/channel/<%= ucid %>/playlists"><%= translate(locale, "Playlists") %></a>
-            <% 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>
+
+        <%= Invidious::Frontend::ChannelPage.generate_tabs_links(locale, channel, selected_tab) %>
     </div>
     <div class="pure-u-1-3"></div>
     <div class="pure-u-1-3">
@@ -111,7 +96,12 @@
 </div>
 
 <div class="pure-g h-box">
-    <div class="pure-u-1 pure-u-lg-1-5"></div>
-    <div class="pure-u-1 pure-u-lg-3-5"></div>
-    <div class="pure-u-1 pure-u-lg-1-5"></div>
+    <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 next_continuation %>
+            <a href="/channel/<%= ucid %>/<%= selected_tab %>?continuation=<%= next_continuation %><% if sort_by != "last" %>&sort_by=<%= URI.encode_www_form(sort_by) %><% end %>">
+                <%= translate(locale, "Next page") %>
+            </a>
+        <% end %>
+    </div>
 </div>
diff --git a/src/invidious/views/community.ecr b/src/invidious/views/community.ecr
index 3bc29e55..e467a679 100644
--- a/src/invidious/views/community.ecr
+++ b/src/invidious/views/community.ecr
@@ -50,19 +50,8 @@
                 <a href="https://redirect.invidious.io<%= env.request.resource %>"><%= translate(locale, "Switch Invidious Instance") %></a>
             <% end %>
         </div>
-        <% if !channel.auto_generated %>
-            <div class="pure-u-1 pure-md-1-3">
-                <a href="/channel/<%= channel.ucid %>"><%= translate(locale, "Videos") %></a>
-            </div>
-        <% end %>
-        <div class="pure-u-1 pure-md-1-3">
-            <a href="/channel/<%= channel.ucid %>/playlists"><%= translate(locale, "Playlists") %></a>
-        </div>
-        <div class="pure-u-1 pure-md-1-3">
-            <% if channel.tabs.includes? "community" %>
-                <b><%= translate(locale, "Community") %></b>
-            <% end %>
-        </div>
+
+        <%= Invidious::Frontend::ChannelPage.generate_tabs_links(locale, channel, Invidious::Frontend::ChannelPage::TabsAvailable::Community) %>
     </div>
     <div class="pure-u-2-3"></div>
 </div>
diff --git a/src/invidious/views/playlists.ecr b/src/invidious/views/playlists.ecr
index c8718e7b..56d25ef5 100644
--- a/src/invidious/views/playlists.ecr
+++ b/src/invidious/views/playlists.ecr
@@ -54,19 +54,7 @@
             <% 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>
+        <%= Invidious::Frontend::ChannelPage.generate_tabs_links(locale, channel, Invidious::Frontend::ChannelPage::TabsAvailable::Playlists) %>
     </div>
     <div class="pure-u-1-3"></div>
     <div class="pure-u-1-3">