Comments: Add support for new format (#4576)

The new comment format is similar to the description's commandRuns.

This should fix the issues with most comments but there are still
some more changes that would need to be made like adding support for
formatting (bold, italic, underline) and channel emojis.

Fixes issue 4566
pull/4591/merge
Samantaz Fox 1 month ago
commit 7c1d2714e0
No known key found for this signature in database
GPG Key ID: F42821059186176E

@ -64,15 +64,15 @@ def content_to_comment_html(content, video_id : String? = "")
# check for custom emojis # check for custom emojis
if run["emoji"]? if run["emoji"]?
if run["emoji"]["isCustomEmoji"]?.try &.as_bool if run["emoji"]["isCustomEmoji"]?.try &.as_bool
if emojiImage = run.dig?("emoji", "image") if emoji_image = run.dig?("emoji", "image")
emojiAlt = emojiImage.dig?("accessibility", "accessibilityData", "label").try &.as_s || text emoji_alt = emoji_image.dig?("accessibility", "accessibilityData", "label").try &.as_s || text
emojiThumb = emojiImage["thumbnails"][0] emoji_thumb = emoji_image["thumbnails"][0]
text = String.build do |str| text = String.build do |str|
str << %(<img alt=") << emojiAlt << "\" " str << %(<img alt=") << emoji_alt << "\" "
str << %(src="/ggpht) << URI.parse(emojiThumb["url"].as_s).request_target << "\" " str << %(src="/ggpht) << URI.parse(emoji_thumb["url"].as_s).request_target << "\" "
str << %(title=") << emojiAlt << "\" " str << %(title=") << emoji_alt << "\" "
str << %(width=") << emojiThumb["width"] << "\" " str << %(width=") << emoji_thumb["width"] << "\" "
str << %(height=") << emojiThumb["height"] << "\" " str << %(height=") << emoji_thumb["height"] << "\" "
str << %(class="channel-emoji" />) str << %(class="channel-emoji" />)
end end
else else

@ -57,7 +57,7 @@ module Invidious::Comments
return initial_data return initial_data
end end
def parse_youtube(id, response, format, locale, thin_mode, sort_by = "top", isPost = false) def parse_youtube(id, response, format, locale, thin_mode, sort_by = "top", is_post = false)
contents = nil contents = nil
if on_response_received_endpoints = response["onResponseReceivedEndpoints"]? if on_response_received_endpoints = response["onResponseReceivedEndpoints"]?
@ -104,6 +104,8 @@ module Invidious::Comments
end end
end end
mutations = response.dig?("frameworkUpdates", "entityBatchUpdate", "mutations").try &.as_a || [] of JSON::Any
response = JSON.build do |json| response = JSON.build do |json|
json.object do json.object do
if header if header
@ -113,7 +115,7 @@ module Invidious::Comments
json.field "commentCount", comment_count json.field "commentCount", comment_count
end end
if isPost if is_post
json.field "postId", id json.field "postId", id
else else
json.field "videoId", id json.field "videoId", id
@ -131,73 +133,138 @@ module Invidious::Comments
node_replies = node["replies"]["commentRepliesRenderer"] node_replies = node["replies"]["commentRepliesRenderer"]
end end
if node["comment"]? if cvm = node["commentViewModel"]?
node_comment = node["comment"]["commentRenderer"] # two commentViewModels for inital request
else # one commentViewModel when getting a replies to a comment
node_comment = node["commentRenderer"] cvm = cvm["commentViewModel"] if cvm["commentViewModel"]?
end
comment_key = cvm["commentKey"]
toolbar_key = cvm["toolbarStateKey"]
comment_mutation = mutations.find { |i| i.dig?("payload", "commentEntityPayload", "key") == comment_key }
toolbar_mutation = mutations.find { |i| i.dig?("entityKey") == toolbar_key }
if !comment_mutation.nil? && !toolbar_mutation.nil?
# todo parse styleRuns, commandRuns and attachmentRuns for comments
html_content = parse_description(comment_mutation.dig("payload", "commentEntityPayload", "properties", "content"), id)
comment_author = comment_mutation.dig("payload", "commentEntityPayload", "author")
json.field "authorId", comment_author["channelId"].as_s
json.field "authorUrl", "/channel/#{comment_author["channelId"].as_s}"
json.field "author", comment_author["displayName"].as_s
json.field "verified", comment_author["isVerified"].as_bool
json.field "authorThumbnails" do
json.array do
comment_mutation.dig?("payload", "commentEntityPayload", "avatar", "image", "sources").try &.as_a.each do |thumbnail|
json.object do
json.field "url", thumbnail["url"]
json.field "width", thumbnail["width"]
json.field "height", thumbnail["height"]
end
end
end
end
content_html = node_comment["contentText"]?.try { |t| parse_content(t, id) } || "" json.field "authorIsChannelOwner", comment_author["isCreator"].as_bool
author = node_comment["authorText"]?.try &.["simpleText"]? || "" json.field "isSponsor", (comment_author["sponsorBadgeUrl"]? != nil)
json.field "verified", (node_comment["authorCommentBadge"]? != nil) if sponsor_badge_url = comment_author["sponsorBadgeUrl"]?
# Sponsor icon thumbnails always have one object and there's only ever the url property in it
json.field "sponsorIconUrl", sponsor_badge_url
end
json.field "author", author comment_toolbar = comment_mutation.dig("payload", "commentEntityPayload", "toolbar")
json.field "authorThumbnails" do json.field "likeCount", short_text_to_number(comment_toolbar["likeCountNotliked"].as_s)
json.array do reply_count = short_text_to_number(comment_toolbar["replyCount"]?.try &.as_s || "0")
node_comment["authorThumbnail"]["thumbnails"].as_a.each do |thumbnail|
json.object do if heart_state = toolbar_mutation.dig?("payload", "engagementToolbarStateEntityPayload", "heartState")
json.field "url", thumbnail["url"] if heart_state.as_s == "TOOLBAR_HEART_STATE_HEARTED"
json.field "width", thumbnail["width"] json.field "creatorHeart" do
json.field "height", thumbnail["height"] json.object do
json.field "creatorThumbnail", comment_toolbar["creatorThumbnailUrl"].as_s
json.field "creatorName", comment_toolbar["heartActiveTooltip"].as_s.sub("❤ by ", "")
end
end
end end
end end
published_text = comment_mutation.dig?("payload", "commentEntityPayload", "properties", "publishedTime").try &.as_s
end end
end
if node_comment["authorEndpoint"]? json.field "isPinned", (cvm.dig?("pinnedText") != nil)
json.field "authorId", node_comment["authorEndpoint"]["browseEndpoint"]["browseId"] json.field "commentId", cvm["commentId"]
json.field "authorUrl", node_comment["authorEndpoint"]["browseEndpoint"]["canonicalBaseUrl"]
else else
json.field "authorId", "" if node["comment"]?
json.field "authorUrl", "" node_comment = node["comment"]["commentRenderer"]
end else
node_comment = node["commentRenderer"]
end
json.field "commentId", node_comment["commentId"]
html_content = node_comment["contentText"]?.try { |t| parse_content(t, id) }
json.field "verified", (node_comment["authorCommentBadge"]? != nil)
json.field "author", node_comment["authorText"]?.try &.["simpleText"]? || ""
json.field "authorThumbnails" do
json.array do
node_comment["authorThumbnail"]["thumbnails"].as_a.each do |thumbnail|
json.object do
json.field "url", thumbnail["url"]
json.field "width", thumbnail["width"]
json.field "height", thumbnail["height"]
end
end
end
end
if comment_action_buttons_renderer = node_comment.dig?("actionButtons", "commentActionButtonsRenderer")
json.field "likeCount", comment_action_buttons_renderer["likeButton"]["toggleButtonRenderer"]["accessibilityData"]["accessibilityData"]["label"].as_s.scan(/\d/).map(&.[0]).join.to_i
if comment_action_buttons_renderer["creatorHeart"]?
heart_data = comment_action_buttons_renderer["creatorHeart"]["creatorHeartRenderer"]["creatorThumbnail"]
json.field "creatorHeart" do
json.object do
json.field "creatorThumbnail", heart_data["thumbnails"][-1]["url"]
json.field "creatorName", heart_data["accessibility"]["accessibilityData"]["label"]
end
end
end
end
published_text = node_comment["publishedTimeText"]["runs"][0]["text"].as_s if node_comment["authorEndpoint"]?
published = decode_date(published_text.rchop(" (edited)")) json.field "authorId", node_comment["authorEndpoint"]["browseEndpoint"]["browseId"]
json.field "authorUrl", node_comment["authorEndpoint"]["browseEndpoint"]["canonicalBaseUrl"]
else
json.field "authorId", ""
json.field "authorUrl", ""
end
if published_text.includes?(" (edited)") json.field "authorIsChannelOwner", node_comment["authorIsChannelOwner"]
json.field "isEdited", true json.field "isPinned", (node_comment["pinnedCommentBadge"]? != nil)
else published_text = node_comment["publishedTimeText"]["runs"][0]["text"].as_s
json.field "isEdited", false
end
json.field "content", html_to_content(content_html) json.field "isSponsor", (node_comment["sponsorCommentBadge"]? != nil)
json.field "contentHtml", content_html if node_comment["sponsorCommentBadge"]?
# Sponsor icon thumbnails always have one object and there's only ever the url property in it
json.field "sponsorIconUrl", node_comment.dig("sponsorCommentBadge", "sponsorCommentBadgeRenderer", "customBadge", "thumbnails", 0, "url").to_s
end
json.field "isPinned", (node_comment["pinnedCommentBadge"]? != nil) reply_count = node_comment["replyCount"]?
json.field "isSponsor", (node_comment["sponsorCommentBadge"]? != nil)
if node_comment["sponsorCommentBadge"]?
# Sponsor icon thumbnails always have one object and there's only ever the url property in it
json.field "sponsorIconUrl", node_comment.dig("sponsorCommentBadge", "sponsorCommentBadgeRenderer", "customBadge", "thumbnails", 0, "url").to_s
end end
json.field "published", published.to_unix
json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale))
comment_action_buttons_renderer = node_comment["actionButtons"]["commentActionButtonsRenderer"] content_html = html_content || ""
json.field "content", html_to_content(content_html)
json.field "likeCount", comment_action_buttons_renderer["likeButton"]["toggleButtonRenderer"]["accessibilityData"]["accessibilityData"]["label"].as_s.scan(/\d/).map(&.[0]).join.to_i json.field "contentHtml", content_html
json.field "commentId", node_comment["commentId"]
json.field "authorIsChannelOwner", node_comment["authorIsChannelOwner"]
if comment_action_buttons_renderer["creatorHeart"]? if published_text != nil
hearth_data = comment_action_buttons_renderer["creatorHeart"]["creatorHeartRenderer"]["creatorThumbnail"] published_text = published_text.to_s
json.field "creatorHeart" do if published_text.includes?(" (edited)")
json.object do json.field "isEdited", true
json.field "creatorThumbnail", hearth_data["thumbnails"][-1]["url"] published = decode_date(published_text.rchop(" (edited)"))
json.field "creatorName", hearth_data["accessibility"]["accessibilityData"]["label"] else
end json.field "isEdited", false
published = decode_date(published_text)
end end
json.field "published", published.to_unix
json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale))
end end
if node_replies && !response["commentRepliesContinuation"]? if node_replies && !response["commentRepliesContinuation"]?
@ -210,7 +277,7 @@ module Invidious::Comments
json.field "replies" do json.field "replies" do
json.object do json.object do
json.field "replyCount", node_comment["replyCount"]? || 1 json.field "replyCount", reply_count || 1
json.field "continuation", continuation json.field "continuation", continuation
end end
end end
@ -236,7 +303,6 @@ module Invidious::Comments
if format == "html" if format == "html"
response = JSON.parse(response) response = JSON.parse(response)
content_html = Frontend::Comments.template_youtube(response, locale, thin_mode) content_html = Frontend::Comments.template_youtube(response, locale, thin_mode)
response = JSON.build do |json| response = JSON.build do |json|
json.object do json.object do
json.field "contentHtml", content_html json.field "contentHtml", content_html

@ -394,7 +394,7 @@ module Invidious::Routes::API::V1::Channels
else else
comments = YoutubeAPI.browse(continuation: continuation) comments = YoutubeAPI.browse(continuation: continuation)
end end
return Comments.parse_youtube(id, comments, format, locale, thin_mode, isPost: true) return Comments.parse_youtube(id, comments, format, locale, thin_mode, is_post: true)
end end
def self.channels(env) def self.channels(env)

@ -231,7 +231,7 @@ module Invidious::Routes::Channels
if nojs if nojs
comments = Comments.fetch_community_post_comments(ucid, id) comments = Comments.fetch_community_post_comments(ucid, id)
comment_html = JSON.parse(Comments.parse_youtube(id, comments, "html", locale, thin_mode, isPost: true))["contentHtml"] comment_html = JSON.parse(Comments.parse_youtube(id, comments, "html", locale, thin_mode, is_post: true))["contentHtml"]
end end
templated "post" templated "post"
end end

@ -7,7 +7,19 @@ private def copy_string(str : String::Builder, iter : Iterator, count : Int) : I
cp = iter.next cp = iter.next
break if cp.is_a?(Iterator::Stop) break if cp.is_a?(Iterator::Stop)
str << cp.chr if cp == 0x26 # Ampersand (&)
str << "&amp;"
elsif cp == 0x27 # Single quote (')
str << "&#39;"
elsif cp == 0x22 # Double quote (")
str << "&quot;"
elsif cp == 0x3C # Less-than (<)
str << "&lt;"
elsif cp == 0x3E # Greater than (>)
str << "&gt;"
else
str << cp.chr
end
# A codepoint from the SMP counts twice # A codepoint from the SMP counts twice
copied += 1 if cp > 0xFFFF copied += 1 if cp > 0xFFFF

Loading…
Cancel
Save