summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--assets/css/default.css10
-rw-r--r--assets/js/comments.js174
-rw-r--r--assets/js/post.js3
-rw-r--r--assets/js/watch.js157
-rw-r--r--src/invidious/channels/community.cr37
-rw-r--r--src/invidious/comments/youtube.cr51
-rw-r--r--src/invidious/frontend/comments_youtube.cr18
-rw-r--r--src/invidious/routes/api/v1/channels.cr53
-rw-r--r--src/invidious/routes/api/v1/misc.cr11
-rw-r--r--src/invidious/routes/channels.cr43
-rw-r--r--src/invidious/routing.cr6
-rw-r--r--src/invidious/views/community.ecr2
-rw-r--r--src/invidious/views/post.ecr48
-rw-r--r--src/invidious/views/watch.ecr6
14 files changed, 450 insertions, 169 deletions
diff --git a/assets/css/default.css b/assets/css/default.css
index c31b24e5..b4053b5c 100644
--- a/assets/css/default.css
+++ b/assets/css/default.css
@@ -392,11 +392,19 @@ p.video-data { margin: 0; font-weight: bold; font-size: 80%; }
* Comments & community posts
*/
-#comments {
+.comments {
max-width: 800px;
margin: auto;
}
+/*
+ * We don't want the top and bottom margin on the post page.
+ */
+.comments.post-comments {
+ margin-bottom: 0;
+ margin-top: 0;
+}
+
.video-iframe-wrapper {
position: relative;
height: 0;
diff --git a/assets/js/comments.js b/assets/js/comments.js
new file mode 100644
index 00000000..35ffa96e
--- /dev/null
+++ b/assets/js/comments.js
@@ -0,0 +1,174 @@
+var video_data = JSON.parse(document.getElementById('video_data').textContent);
+
+var spinnerHTML = '<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
+var spinnerHTMLwithHR = spinnerHTML + '<hr>';
+
+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 = ' \
+ <div> \
+ <h3> \
+ <a href="javascript:void(0)">[ − ]</a> \
+ {commentsText} \
+ </h3> \
+ <b> \
+ '
+ if (video_data.support_reddit) {
+ commentInnerHtml += ' <a href="javascript:void(0)" data-comments="reddit"> \
+ {redditComments} \
+ </a> \
+ '
+ }
+ commentInnerHtml += ' </b> \
+ </div> \
+ <div>{contentHtml}</div> \
+ <hr>'
+ 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 = '<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
-var spinnerHTMLwithHR = spinnerHTML + '<hr>';
-
-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 = ' \
- <div> \
- <h3> \
- <a href="javascript:void(0)">[ − ]</a> \
- {commentsText} \
- </h3> \
- <b> \
- <a href="javascript:void(0)" data-comments="reddit"> \
- {redditComments} \
- </a> \
- </b> \
- </div> \
- <div>{contentHtml}</div> \
- <hr>'.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..49ffd990 100644
--- a/src/invidious/channels/community.cr
+++ b/src/invidious/channels/community.cr
@@ -24,7 +24,33 @@ 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, post_id, locale, format, thin_mode)
+ object = {
+ "2:string" => "community",
+ "25:embedded" => {
+ "22:string" => post_id.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) }
+
+ 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 +65,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 +269,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..185d8e43 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, post_id)
+ object = {
+ "2:string" => "community",
+ "25:embedded" => {
+ "22:string" => post_id,
+ },
+ "45:embedded" => {
+ "2:varint" => 1_i64,
+ "3:varint" => 1_i64,
+ },
+ "53:embedded" => {
+ "4:embedded" => {
+ "6:varint" => 0_i64,
+ "27:varint" => 1_i64,
+ "29:string" => post_id,
+ "30:string" => ucid,
+ },
+ "8:string" => "comments-section",
+ },
+ }
+
+ object_parsed = 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" => object_parsed,
+ },
+ }
+
+ 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
</div>
</div>
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
+ <div class="pure-g">
+ <div class="pure-u-1-24"></div>
+ <div class="pure-u-23-24">
+ <p>
+ <a href="/post/#{child["commentId"]}?ucid=#{comments["authorId"]}">#{replies_count_text}</a>
+ </p>
+ </div>
+ </div>
+ 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..67018660 100644
--- a/src/invidious/routes/api/v1/channels.cr
+++ b/src/invidious/routes/api/v1/channels.cr
@@ -343,6 +343,59 @@ 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"
+
+ if ucid.nil?
+ response = YoutubeAPI.resolve_url("https://www.youtube.com/post/#{id}")
+ return error_json(400, "Invalid post ID") if response["error"]?
+ ucid = response.dig("endpoint", "browseEndpoint", "browseId").as_s
+ else
+ ucid = ucid.to_s
+ end
+
+ 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
+
+ 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..8a92e160 100644
--- a/src/invidious/routes/api/v1/misc.cr
+++ b/src/invidious/routes/api/v1/misc.cr
@@ -162,17 +162,20 @@ 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")
- elsif pageType == "WEB_PAGE_TYPE_UNKNOWN"
+ if pageType == "WEB_PAGE_TYPE_UNKNOWN"
return error_json(400, "Unknown url")
end
+
+ sub_endpoint = endpoint["watchEndpoint"]? || endpoint["browseEndpoint"]? || endpoint
+ params = sub_endpoint.try &.dig?("params")
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 "ucid", sub_endpoint["browseId"].as_s if sub_endpoint["browseId"]?
+ json.field "videoId", sub_endpoint["videoId"].as_s if sub_endpoint["videoId"]?
+ 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 d744712d..d4d8b1c1 100644
--- a/src/invidious/routes/channels.cr
+++ b/src/invidious/routes/channels.cr
@@ -165,6 +165,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/#{URI.encode_www_form(lb)}?ucid=#{URI.encode_www_form(ucid)}"
+ end
+
thin_mode = env.params.query["thin_mode"]? || env.get("preferences").as(Preferences).thin_mode
thin_mode = thin_mode == "true"
@@ -193,6 +198,44 @@ 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
+
+ thin_mode = env.params.query["thin_mode"]? || prefs.thin_mode
+ thin_mode = thin_mode == "true"
+
+ nojs = env.params.query["nojs"]?
+
+ nojs ||= "0"
+ nojs = nojs == "1"
+
+ 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
+ post_response = fetch_channel_community_post(ucid, id, locale, "json", thin_mode)
+ end
+
+ post_response = JSON.parse(post_response)
+
+ if nojs
+ comments = Comments.fetch_community_post_comments(ucid, id)
+ comment_html = JSON.parse(Comments.parse_youtube(id, comments, "html", locale, thin_mode, isPost: true))["contentHtml"]
+ end
+ 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 8c38aaed..d6bd991c 100644
--- a/src/invidious/routing.cr
+++ b/src/invidious/routing.cr
@@ -128,6 +128,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
# Channel catch-all, to redirect future routes to the channel's home
# NOTE: defined last in order to be processed after the other routes
@@ -253,6 +254,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
@@ -262,6 +267,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 @@
<p><%= error_message %></p>
</div>
<% else %>
- <div class="h-box pure-g" id="comments">
+ <div class="h-box pure-g comments" id="comments">
<%= IV::Frontend::Comments.template_youtube(items.not_nil!, locale, thin_mode) %>
</div>
<% end %>
diff --git a/src/invidious/views/post.ecr b/src/invidious/views/post.ecr
new file mode 100644
index 00000000..fb03a44c
--- /dev/null
+++ b/src/invidious/views/post.ecr
@@ -0,0 +1,48 @@
+<% content_for "header" do %>
+<title>Invidious</title>
+<% end %>
+
+<div>
+ <div id="post" class="comments post-comments">
+ <%= IV::Frontend::Comments.template_youtube(post_response.not_nil!, locale, thin_mode) %>
+ </div>
+
+ <% if nojs %>
+ <hr>
+ <% end %>
+ <br />
+
+ <div id="comments" class="comments post-comments">
+ <% if nojs %>
+ <%= comment_html %>
+ <% else %>
+ <noscript>
+ <a href="/post/<%= id %>?ucid=<%= ucid %>&nojs=1">
+ <%= translate(locale, "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.") %>
+ </a>
+ </noscript>
+ <% end %>
+ </div>
+</div>
+
+<script id="video_data" type="application/json">
+<%=
+{
+ "id" => id,
+ "youtube_comments_text" => HTML.escape(translate(locale, "View YouTube comments")),
+ "reddit_comments_text" => "",
+ "reddit_permalink_text" => "",
+ "comments_text" => HTML.escape(translate(locale, "View `x` comments", "{commentCount}")),
+ "hide_replies_text" => HTML.escape(translate(locale, "Hide replies")),
+ "show_replies_text" => HTML.escape(translate(locale, "Show replies")),
+ "params" => {
+ "comments": ["youtube"]
+ },
+ "preferences" => prefs,
+ "base_url" => "/api/v1/post/#{URI.encode_www_form(id)}/comments",
+ "ucid" => ucid
+}.to_pretty_json
+%>
+</script>
+<script src="/js/comments.js?v=<%= ASSET_COMMIT %>"></script>
+<script src="/js/post.js?v=<%= ASSET_COMMIT %>"></script> \ No newline at end of file
diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr
index 498d57a1..62a154a4 100644
--- a/src/invidious/views/watch.ecr
+++ b/src/invidious/views/watch.ecr
@@ -64,7 +64,8 @@ we're going to need to do it here in order to allow for translations.
"premiere_timestamp" => video.premiere_timestamp.try &.to_unix,
"vr" => video.is_vr,
"projection_type" => video.projection_type,
- "local_disabled" => CONFIG.disabled?("local")
+ "local_disabled" => CONFIG.disabled?("local"),
+ "support_reddit" => true
}.to_pretty_json
%>
</script>
@@ -270,7 +271,7 @@ we're going to need to do it here in order to allow for translations.
<hr>
<% end %>
- <div id="comments">
+ <div id="comments" class="comments">
<% if nojs %>
<%= comment_html %>
<% else %>
@@ -352,4 +353,5 @@ we're going to need to do it here in order to allow for translations.
</div>
<% end %>
</div>
+<script src="/js/comments.js?v=<%= ASSET_COMMIT %>"></script>
<script src="/js/watch.js?v=<%= ASSET_COMMIT %>"></script>