From e3c365f3d68985a8e3ce35f76d334c7184a548de Mon Sep 17 00:00:00 2001
From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com>
Date: Wed, 19 Jul 2023 12:35:22 -0700
Subject: [PATCH 1/6] Add support for post page
---
assets/css/default.css | 2 +-
assets/js/comments.js | 174 +++++++++++++++++++++
assets/js/post.js | 3 +
assets/js/watch.js | 157 -------------------
src/invidious/channels/community.cr | 39 ++++-
src/invidious/comments/youtube.cr | 51 +++++-
src/invidious/frontend/comments_youtube.cr | 18 +++
src/invidious/routes/api/v1/channels.cr | 47 ++++++
src/invidious/routes/api/v1/misc.cr | 10 +-
src/invidious/routes/channels.cr | 37 +++++
src/invidious/routing.cr | 6 +
src/invidious/views/community.ecr | 2 +-
src/invidious/views/post.ecr | 31 ++++
src/invidious/views/watch.ecr | 6 +-
14 files changed, 416 insertions(+), 167 deletions(-)
create mode 100644 assets/js/comments.js
create mode 100644 assets/js/post.js
create mode 100644 src/invidious/views/post.ecr
diff --git a/assets/css/default.css b/assets/css/default.css
index c31b24e5..69fe8d5f 100644
--- a/assets/css/default.css
+++ b/assets/css/default.css
@@ -392,7 +392,7 @@ p.video-data { margin: 0; font-weight: bold; font-size: 80%; }
* Comments & community posts
*/
-#comments {
+.comments {
max-width: 800px;
margin: auto;
}
diff --git a/assets/js/comments.js b/assets/js/comments.js
new file mode 100644
index 00000000..00a8cae9
--- /dev/null
+++ b/assets/js/comments.js
@@ -0,0 +1,174 @@
+var video_data = JSON.parse(document.getElementById('video_data').textContent);
+
+var spinnerHTML = '
';
+var spinnerHTMLwithHR = spinnerHTML + '
';
+
+String.prototype.supplant = function (o) {
+ return this.replace(/{([^{}]*)}/g, function (a, b) {
+ var r = o[b];
+ return typeof r === 'string' || typeof r === 'number' ? r : a;
+ });
+};
+
+function toggle_comments(event) {
+ var target = event.target;
+ var body = target.parentNode.parentNode.parentNode.children[1];
+ if (body.style.display === 'none') {
+ target.textContent = '[ − ]';
+ body.style.display = '';
+ } else {
+ target.textContent = '[ + ]';
+ body.style.display = 'none';
+ }
+}
+
+function hide_youtube_replies(event) {
+ var target = event.target;
+
+ var sub_text = target.getAttribute('data-inner-text');
+ var inner_text = target.getAttribute('data-sub-text');
+
+ var body = target.parentNode.parentNode.children[1];
+ body.style.display = 'none';
+
+ target.textContent = sub_text;
+ target.onclick = show_youtube_replies;
+ target.setAttribute('data-inner-text', inner_text);
+ target.setAttribute('data-sub-text', sub_text);
+}
+
+function show_youtube_replies(event) {
+ var target = event.target;
+
+ var sub_text = target.getAttribute('data-inner-text');
+ var inner_text = target.getAttribute('data-sub-text');
+
+ var body = target.parentNode.parentNode.children[1];
+ body.style.display = '';
+
+ target.textContent = sub_text;
+ target.onclick = hide_youtube_replies;
+ target.setAttribute('data-inner-text', inner_text);
+ target.setAttribute('data-sub-text', sub_text);
+}
+
+function get_youtube_comments() {
+ var comments = document.getElementById('comments');
+
+ var fallback = comments.innerHTML;
+ comments.innerHTML = spinnerHTML;
+
+ var baseUrl = video_data.base_url || '/api/v1/comments/'+ video_data.id
+ var url = baseUrl +
+ '?format=html' +
+ '&hl=' + video_data.preferences.locale +
+ '&thin_mode=' + video_data.preferences.thin_mode;
+
+ if (video_data.ucid) {
+ url += '&ucid=' + video_data.ucid
+ }
+
+ var onNon200 = function (xhr) { comments.innerHTML = fallback; };
+ if (video_data.params.comments[1] === 'youtube')
+ onNon200 = function (xhr) {};
+
+ helpers.xhr('GET', url, {retries: 5, entity_name: 'comments'}, {
+ on200: function (response) {
+ var commentInnerHtml = ' \
+ \
+
\
+ [ − ] \
+ {commentsText} \
+
\
+
\
+ '
+ if (video_data.support_reddit) {
+ commentInnerHtml += ' \
+ {redditComments} \
+ \
+ '
+ }
+ commentInnerHtml += ' \
+
\
+ {contentHtml}
\
+
'
+ commentInnerHtml = commentInnerHtml.supplant({
+ contentHtml: response.contentHtml,
+ redditComments: video_data.reddit_comments_text,
+ commentsText: video_data.comments_text.supplant({
+ // toLocaleString correctly splits number with local thousands separator. e.g.:
+ // '1,234,567.89' for user with English locale
+ // '1 234 567,89' for user with Russian locale
+ // '1.234.567,89' for user with Portuguese locale
+ commentCount: response.commentCount.toLocaleString()
+ })
+ });
+ comments.innerHTML = commentInnerHtml;
+ comments.children[0].children[0].children[0].onclick = toggle_comments;
+ if (video_data.support_reddit) {
+ comments.children[0].children[1].children[0].onclick = swap_comments;
+ }
+ },
+ onNon200: onNon200, // declared above
+ onError: function (xhr) {
+ comments.innerHTML = spinnerHTML;
+ },
+ onTimeout: function (xhr) {
+ comments.innerHTML = spinnerHTML;
+ }
+ });
+}
+
+function get_youtube_replies(target, load_more, load_replies) {
+ var continuation = target.getAttribute('data-continuation');
+
+ var body = target.parentNode.parentNode;
+ var fallback = body.innerHTML;
+ body.innerHTML = spinnerHTML;
+ var baseUrl = video_data.base_url || '/api/v1/comments/'+ video_data.id
+ var url = baseUrl +
+ '?format=html' +
+ '&hl=' + video_data.preferences.locale +
+ '&thin_mode=' + video_data.preferences.thin_mode +
+ '&continuation=' + continuation;
+
+ if (video_data.ucid) {
+ url += '&ucid=' + video_data.ucid
+ }
+ if (load_replies) url += '&action=action_get_comment_replies';
+
+ helpers.xhr('GET', url, {}, {
+ on200: function (response) {
+ if (load_more) {
+ body = body.parentNode.parentNode;
+ body.removeChild(body.lastElementChild);
+ body.insertAdjacentHTML('beforeend', response.contentHtml);
+ } else {
+ body.removeChild(body.lastElementChild);
+
+ var p = document.createElement('p');
+ var a = document.createElement('a');
+ p.appendChild(a);
+
+ a.href = 'javascript:void(0)';
+ a.onclick = hide_youtube_replies;
+ a.setAttribute('data-sub-text', video_data.hide_replies_text);
+ a.setAttribute('data-inner-text', video_data.show_replies_text);
+ a.textContent = video_data.hide_replies_text;
+
+ var div = document.createElement('div');
+ div.innerHTML = response.contentHtml;
+
+ body.appendChild(p);
+ body.appendChild(div);
+ }
+ },
+ onNon200: function (xhr) {
+ body.innerHTML = fallback;
+ },
+ onTimeout: function (xhr) {
+ console.warn('Pulling comments failed');
+ body.innerHTML = fallback;
+ }
+ });
+}
\ No newline at end of file
diff --git a/assets/js/post.js b/assets/js/post.js
new file mode 100644
index 00000000..fcbc9155
--- /dev/null
+++ b/assets/js/post.js
@@ -0,0 +1,3 @@
+addEventListener('load', function (e) {
+ get_youtube_comments();
+});
diff --git a/assets/js/watch.js b/assets/js/watch.js
index 36506abd..26ad138f 100644
--- a/assets/js/watch.js
+++ b/assets/js/watch.js
@@ -1,14 +1,4 @@
'use strict';
-var video_data = JSON.parse(document.getElementById('video_data').textContent);
-var spinnerHTML = '
';
-var spinnerHTMLwithHR = spinnerHTML + '
';
-
-String.prototype.supplant = function (o) {
- return this.replace(/{([^{}]*)}/g, function (a, b) {
- var r = o[b];
- return typeof r === 'string' || typeof r === 'number' ? r : a;
- });
-};
function toggle_parent(target) {
var body = target.parentNode.parentNode.children[1];
@@ -21,18 +11,6 @@ function toggle_parent(target) {
}
}
-function toggle_comments(event) {
- var target = event.target;
- var body = target.parentNode.parentNode.parentNode.children[1];
- if (body.style.display === 'none') {
- target.textContent = '[ − ]';
- body.style.display = '';
- } else {
- target.textContent = '[ + ]';
- body.style.display = 'none';
- }
-}
-
function swap_comments(event) {
var source = event.target.getAttribute('data-comments');
@@ -43,36 +21,6 @@ function swap_comments(event) {
}
}
-function hide_youtube_replies(event) {
- var target = event.target;
-
- var sub_text = target.getAttribute('data-inner-text');
- var inner_text = target.getAttribute('data-sub-text');
-
- var body = target.parentNode.parentNode.children[1];
- body.style.display = 'none';
-
- target.textContent = sub_text;
- target.onclick = show_youtube_replies;
- target.setAttribute('data-inner-text', inner_text);
- target.setAttribute('data-sub-text', sub_text);
-}
-
-function show_youtube_replies(event) {
- var target = event.target;
-
- var sub_text = target.getAttribute('data-inner-text');
- var inner_text = target.getAttribute('data-sub-text');
-
- var body = target.parentNode.parentNode.children[1];
- body.style.display = '';
-
- target.textContent = sub_text;
- target.onclick = hide_youtube_replies;
- target.setAttribute('data-inner-text', inner_text);
- target.setAttribute('data-sub-text', sub_text);
-}
-
var continue_button = document.getElementById('continue');
if (continue_button) {
continue_button.onclick = continue_autoplay;
@@ -208,111 +156,6 @@ function get_reddit_comments() {
});
}
-function get_youtube_comments() {
- var comments = document.getElementById('comments');
-
- var fallback = comments.innerHTML;
- comments.innerHTML = spinnerHTML;
-
- var url = '/api/v1/comments/' + video_data.id +
- '?format=html' +
- '&hl=' + video_data.preferences.locale +
- '&thin_mode=' + video_data.preferences.thin_mode;
-
- var onNon200 = function (xhr) { comments.innerHTML = fallback; };
- if (video_data.params.comments[1] === 'youtube')
- onNon200 = function (xhr) {};
-
- helpers.xhr('GET', url, {retries: 5, entity_name: 'comments'}, {
- on200: function (response) {
- comments.innerHTML = ' \
- \
- {contentHtml}
\
-
'.supplant({
- contentHtml: response.contentHtml,
- redditComments: video_data.reddit_comments_text,
- commentsText: video_data.comments_text.supplant({
- // toLocaleString correctly splits number with local thousands separator. e.g.:
- // '1,234,567.89' for user with English locale
- // '1 234 567,89' for user with Russian locale
- // '1.234.567,89' for user with Portuguese locale
- commentCount: response.commentCount.toLocaleString()
- })
- });
-
- comments.children[0].children[0].children[0].onclick = toggle_comments;
- comments.children[0].children[1].children[0].onclick = swap_comments;
- },
- onNon200: onNon200, // declared above
- onError: function (xhr) {
- comments.innerHTML = spinnerHTML;
- },
- onTimeout: function (xhr) {
- comments.innerHTML = spinnerHTML;
- }
- });
-}
-
-function get_youtube_replies(target, load_more, load_replies) {
- var continuation = target.getAttribute('data-continuation');
-
- var body = target.parentNode.parentNode;
- var fallback = body.innerHTML;
- body.innerHTML = spinnerHTML;
-
- var url = '/api/v1/comments/' + video_data.id +
- '?format=html' +
- '&hl=' + video_data.preferences.locale +
- '&thin_mode=' + video_data.preferences.thin_mode +
- '&continuation=' + continuation;
- if (load_replies) url += '&action=action_get_comment_replies';
-
- helpers.xhr('GET', url, {}, {
- on200: function (response) {
- if (load_more) {
- body = body.parentNode.parentNode;
- body.removeChild(body.lastElementChild);
- body.insertAdjacentHTML('beforeend', response.contentHtml);
- } else {
- body.removeChild(body.lastElementChild);
-
- var p = document.createElement('p');
- var a = document.createElement('a');
- p.appendChild(a);
-
- a.href = 'javascript:void(0)';
- a.onclick = hide_youtube_replies;
- a.setAttribute('data-sub-text', video_data.hide_replies_text);
- a.setAttribute('data-inner-text', video_data.show_replies_text);
- a.textContent = video_data.hide_replies_text;
-
- var div = document.createElement('div');
- div.innerHTML = response.contentHtml;
-
- body.appendChild(p);
- body.appendChild(div);
- }
- },
- onNon200: function (xhr) {
- body.innerHTML = fallback;
- },
- onTimeout: function (xhr) {
- console.warn('Pulling comments failed');
- body.innerHTML = fallback;
- }
- });
-}
-
if (video_data.play_next) {
player.on('ended', function () {
var url = new URL('https://example.com/watch?v=' + video_data.next_video);
diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr
index 791f1641..85ddff35 100644
--- a/src/invidious/channels/community.cr
+++ b/src/invidious/channels/community.cr
@@ -24,7 +24,35 @@ def fetch_channel_community(ucid, cursor, locale, format, thin_mode)
return extract_channel_community(items, ucid: ucid, locale: locale, format: format, thin_mode: thin_mode)
end
-def extract_channel_community(items, *, ucid, locale, format, thin_mode)
+def fetch_channel_community_post(ucid, postId, locale, format, thin_mode, params : String | Nil = nil)
+ if params.nil?
+ object = {
+ "2:string" => "community",
+ "25:embedded" => {
+ "22:string" => postId.to_s,
+ },
+ "45:embedded" => {
+ "2:varint" => 1_i64,
+ "3:varint" => 1_i64,
+ },
+ }
+ params = 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) }
+ end
+
+ initial_data = YoutubeAPI.browse(ucid, params: params)
+
+ items = [] of JSON::Any
+ extract_items(initial_data) do |item|
+ items << item
+ end
+
+ return extract_channel_community(items, ucid: ucid, locale: locale, format: format, thin_mode: thin_mode, is_single_post: true)
+end
+
+def extract_channel_community(items, *, ucid, locale, format, thin_mode, is_single_post : Bool = false)
if message = items[0]["messageRenderer"]?
error_message = (message["text"]["simpleText"]? ||
message["text"]["runs"]?.try &.[0]?.try &.["text"]?)
@@ -39,6 +67,9 @@ def extract_channel_community(items, *, ucid, locale, format, thin_mode)
response = JSON.build do |json|
json.object do
json.field "authorId", ucid
+ if is_single_post
+ json.field "singlePost", true
+ end
json.field "comments" do
json.array do
items.each do |post|
@@ -240,8 +271,10 @@ def extract_channel_community(items, *, ucid, locale, format, thin_mode)
end
end
end
- if cont = items.dig?(-1, "continuationItemRenderer", "continuationEndpoint", "continuationCommand", "token")
- json.field "continuation", extract_channel_community_cursor(cont.as_s)
+ if !is_single_post
+ if cont = items.dig?(-1, "continuationItemRenderer", "continuationEndpoint", "continuationCommand", "token")
+ json.field "continuation", extract_channel_community_cursor(cont.as_s)
+ end
end
end
end
diff --git a/src/invidious/comments/youtube.cr b/src/invidious/comments/youtube.cr
index 1ba1b534..da7f0543 100644
--- a/src/invidious/comments/youtube.cr
+++ b/src/invidious/comments/youtube.cr
@@ -13,6 +13,51 @@ module Invidious::Comments
client_config = YoutubeAPI::ClientConfig.new(region: region)
response = YoutubeAPI.next(continuation: ctoken, client_config: client_config)
+ return parse_youtube(id, response, format, locale, thin_mode, sort_by)
+ end
+
+ def fetch_community_post_comments(ucid, postId)
+ object = {
+ "2:string" => "community",
+ "25:embedded" => {
+ "22:string" => postId,
+ },
+ "45:embedded" => {
+ "2:varint" => 1_i64,
+ "3:varint" => 1_i64,
+ },
+ "53:embedded" => {
+ "4:embedded" => {
+ "6:varint" => 0_i64,
+ "27:varint" => 1_i64,
+ "29:string" => postId,
+ "30:string" => ucid,
+ },
+ "8:string" => "comments-section",
+ },
+ }
+
+ objectParsed = object.try { |i| Protodec::Any.cast_json(i) }
+ .try { |i| Protodec::Any.from_json(i) }
+ .try { |i| Base64.urlsafe_encode(i) }
+
+ object2 = {
+ "80226972:embedded" => {
+ "2:string" => ucid,
+ "3:string" => objectParsed,
+ },
+ }
+
+ continuation = object2.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) }
+
+ initial_data = YoutubeAPI.browse(continuation: continuation)
+ return initial_data
+ end
+
+ def parse_youtube(id, response, format, locale, thin_mode, sort_by = "top", isPost = false)
contents = nil
if on_response_received_endpoints = response["onResponseReceivedEndpoints"]?
@@ -68,7 +113,11 @@ module Invidious::Comments
json.field "commentCount", comment_count
end
- json.field "videoId", id
+ if isPost
+ json.field "postId", id
+ else
+ json.field "videoId", id
+ end
json.field "comments" do
json.array do
diff --git a/src/invidious/frontend/comments_youtube.cr b/src/invidious/frontend/comments_youtube.cr
index 41f43f04..ecc0bc1b 100644
--- a/src/invidious/frontend/comments_youtube.cr
+++ b/src/invidious/frontend/comments_youtube.cr
@@ -23,6 +23,24 @@ module Invidious::Frontend::Comments
END_HTML
+ elsif comments["authorId"]? && !comments["singlePost"]?
+ # for posts we should display a link to the post
+ replies_count_text = translate_count(locale,
+ "comments_view_x_replies",
+ child["replyCount"].as_i64 || 0,
+ NumberFormatting::Separator
+ )
+
+ replies_html = <<-END_HTML
+
+ END_HTML
end
if !thin_mode
diff --git a/src/invidious/routes/api/v1/channels.cr b/src/invidious/routes/api/v1/channels.cr
index adf05d30..0d2d2eb1 100644
--- a/src/invidious/routes/api/v1/channels.cr
+++ b/src/invidious/routes/api/v1/channels.cr
@@ -343,6 +343,53 @@ module Invidious::Routes::API::V1::Channels
end
end
+ def self.post(env)
+ locale = env.get("preferences").as(Preferences).locale
+
+ env.response.content_type = "application/json"
+
+ id = env.params.url["id"].to_s
+ ucid = env.params.query["ucid"]
+
+ thin_mode = env.params.query["thin_mode"]?
+ thin_mode = thin_mode == "true"
+
+ format = env.params.query["format"]?
+ format ||= "json"
+
+ begin
+ fetch_channel_community_post(ucid, id, locale, format, thin_mode)
+ rescue ex
+ return error_json(500, ex)
+ end
+ end
+
+ def self.post_comments(env)
+ locale = env.get("preferences").as(Preferences).locale
+ region = env.params.query["region"]?
+
+ env.response.content_type = "application/json"
+
+ id = env.params.url["id"]
+
+ thin_mode = env.params.query["thin_mode"]?
+ thin_mode = thin_mode == "true"
+
+ format = env.params.query["format"]?
+ format ||= "json"
+
+ continuation = env.params.query["continuation"]?
+
+ case continuation
+ when nil, ""
+ ucid = env.params.query["ucid"]
+ comments = Comments.fetch_community_post_comments(ucid, id)
+ else
+ comments = YoutubeAPI.browse(continuation: continuation)
+ end
+ return Comments.parse_youtube(id, comments, format, locale, thin_mode, isPost: true)
+ end
+
def self.channels(env)
locale = env.get("preferences").as(Preferences).locale
ucid = env.params.url["ucid"]
diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr
index e499f4d6..91a62fa3 100644
--- a/src/invidious/routes/api/v1/misc.cr
+++ b/src/invidious/routes/api/v1/misc.cr
@@ -162,17 +162,23 @@ module Invidious::Routes::API::V1::Misc
resolved_url = YoutubeAPI.resolve_url(url.as(String))
endpoint = resolved_url["endpoint"]
pageType = endpoint.dig?("commandMetadata", "webCommandMetadata", "webPageType").try &.as_s || ""
- if resolved_ucid = endpoint.dig?("watchEndpoint", "videoId")
- elsif resolved_ucid = endpoint.dig?("browseEndpoint", "browseId")
+ if sub_endpoint = endpoint.dig?("watchEndpoint")
+ resolved_ucid = sub_endpoint.dig?("videoId")
+ elsif sub_endpoint = endpoint.dig?("browseEndpoint")
+ resolved_ucid = sub_endpoint.dig?("browseId")
elsif pageType == "WEB_PAGE_TYPE_UNKNOWN"
return error_json(400, "Unknown url")
end
+ if !sub_endpoint.nil?
+ params = sub_endpoint.dig?("params")
+ end
rescue ex
return error_json(500, ex)
end
JSON.build do |json|
json.object do
json.field "ucid", resolved_ucid.try &.as_s || ""
+ json.field "params", params.try &.as_s
json.field "pageType", pageType
end
end
diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr
index 9892ae2a..1d02ee08 100644
--- a/src/invidious/routes/channels.cr
+++ b/src/invidious/routes/channels.cr
@@ -159,6 +159,11 @@ module Invidious::Routes::Channels
end
locale, user, subscriptions, continuation, ucid, channel = data
+ # redirect to post page
+ if lb = env.params.query["lb"]?
+ env.redirect "/post/#{lb}?ucid=#{ucid}"
+ end
+
thin_mode = env.params.query["thin_mode"]? || env.get("preferences").as(Preferences).thin_mode
thin_mode = thin_mode == "true"
@@ -187,6 +192,38 @@ module Invidious::Routes::Channels
templated "community"
end
+ def self.post(env)
+ # /post/{postId}
+ id = env.params.url["id"]
+ ucid = env.params.query["ucid"]?
+
+ prefs = env.get("preferences").as(Preferences)
+
+ locale = prefs.locale
+ region = env.params.query["region"]? || prefs.region
+
+ thin_mode = env.params.query["thin_mode"]? || prefs.thin_mode
+ thin_mode = thin_mode == "true"
+
+ client_config = YoutubeAPI::ClientConfig.new(region: region)
+
+ if !ucid.nil?
+ ucid = ucid.to_s
+ post_response = fetch_channel_community_post(ucid, id, locale, "json", thin_mode)
+ else
+ # resolve the url to get the author's UCID
+ response = YoutubeAPI.resolve_url("https://www.youtube.com/post/#{id}")
+ return error_template(400, "Invalid post ID") if response["error"]?
+
+ ucid = response.dig("endpoint", "browseEndpoint", "browseId").as_s
+ params = response.dig("endpoint", "browseEndpoint", "params").as_s
+ post_response = fetch_channel_community_post(ucid, id, locale, "json", thin_mode, params: params)
+ end
+
+ post_response = JSON.parse(post_response)
+ templated "post"
+ end
+
def self.channels(env)
data = self.fetch_basic_information(env)
return data if !data.is_a?(Tuple)
diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr
index 9c43171c..8cb49249 100644
--- a/src/invidious/routing.cr
+++ b/src/invidious/routing.cr
@@ -127,6 +127,7 @@ module Invidious::Routing
get "/channel/:ucid/live", Routes::Channels, :live
get "/user/:user/live", Routes::Channels, :live
get "/c/:user/live", Routes::Channels, :live
+ get "/post/:id", Routes::Channels, :post
{"", "/videos", "/shorts", "/streams", "/playlists", "/community", "/about"}.each do |path|
# /c/LinusTechTips
@@ -240,6 +241,10 @@ module Invidious::Routing
get "/api/v1/channels/:ucid/#{{{route}}}", {{namespace}}::Channels, :{{route}}
{% end %}
+ # Posts
+ get "/api/v1/post/:id", {{namespace}}::Channels, :post
+ get "/api/v1/post/:id/comments", {{namespace}}::Channels, :post_comments
+
# 301 redirects to new /api/v1/channels/community/:ucid and /:ucid/community
get "/api/v1/channels/comments/:ucid", {{namespace}}::Channels, :channel_comments_redirect
get "/api/v1/channels/:ucid/comments", {{namespace}}::Channels, :channel_comments_redirect
@@ -249,6 +254,7 @@ module Invidious::Routing
get "/api/v1/search/suggestions", {{namespace}}::Search, :search_suggestions
get "/api/v1/hashtag/:hashtag", {{namespace}}::Search, :hashtag
+
# Authenticated
# The notification APIs cannot be extracted yet! They require the *local* notifications constant defined in invidious.cr
diff --git a/src/invidious/views/community.ecr b/src/invidious/views/community.ecr
index 24efc34e..d2a305d3 100644
--- a/src/invidious/views/community.ecr
+++ b/src/invidious/views/community.ecr
@@ -26,7 +26,7 @@
<%= error_message %>
<% else %>
-