mirror of
https://gitea.invidious.io/iv-org/invidious.git
synced 2024-08-15 00:53:41 +00:00
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
This commit is contained in:
commit
7c1d2714e0
5 changed files with 148 additions and 70 deletions
|
@ -64,15 +64,15 @@ def content_to_comment_html(content, video_id : String? = "")
|
|||
# check for custom emojis
|
||||
if run["emoji"]?
|
||||
if run["emoji"]["isCustomEmoji"]?.try &.as_bool
|
||||
if emojiImage = run.dig?("emoji", "image")
|
||||
emojiAlt = emojiImage.dig?("accessibility", "accessibilityData", "label").try &.as_s || text
|
||||
emojiThumb = emojiImage["thumbnails"][0]
|
||||
if emoji_image = run.dig?("emoji", "image")
|
||||
emoji_alt = emoji_image.dig?("accessibility", "accessibilityData", "label").try &.as_s || text
|
||||
emoji_thumb = emoji_image["thumbnails"][0]
|
||||
text = String.build do |str|
|
||||
str << %(<img alt=") << emojiAlt << "\" "
|
||||
str << %(src="/ggpht) << URI.parse(emojiThumb["url"].as_s).request_target << "\" "
|
||||
str << %(title=") << emojiAlt << "\" "
|
||||
str << %(width=") << emojiThumb["width"] << "\" "
|
||||
str << %(height=") << emojiThumb["height"] << "\" "
|
||||
str << %(<img alt=") << emoji_alt << "\" "
|
||||
str << %(src="/ggpht) << URI.parse(emoji_thumb["url"].as_s).request_target << "\" "
|
||||
str << %(title=") << emoji_alt << "\" "
|
||||
str << %(width=") << emoji_thumb["width"] << "\" "
|
||||
str << %(height=") << emoji_thumb["height"] << "\" "
|
||||
str << %(class="channel-emoji" />)
|
||||
end
|
||||
else
|
||||
|
|
|
@ -57,7 +57,7 @@ module Invidious::Comments
|
|||
return initial_data
|
||||
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
|
||||
|
||||
if on_response_received_endpoints = response["onResponseReceivedEndpoints"]?
|
||||
|
@ -104,6 +104,8 @@ module Invidious::Comments
|
|||
end
|
||||
end
|
||||
|
||||
mutations = response.dig?("frameworkUpdates", "entityBatchUpdate", "mutations").try &.as_a || [] of JSON::Any
|
||||
|
||||
response = JSON.build do |json|
|
||||
json.object do
|
||||
if header
|
||||
|
@ -113,7 +115,7 @@ module Invidious::Comments
|
|||
json.field "commentCount", comment_count
|
||||
end
|
||||
|
||||
if isPost
|
||||
if is_post
|
||||
json.field "postId", id
|
||||
else
|
||||
json.field "videoId", id
|
||||
|
@ -131,73 +133,138 @@ module Invidious::Comments
|
|||
node_replies = node["replies"]["commentRepliesRenderer"]
|
||||
end
|
||||
|
||||
if node["comment"]?
|
||||
node_comment = node["comment"]["commentRenderer"]
|
||||
if cvm = node["commentViewModel"]?
|
||||
# two commentViewModels for inital request
|
||||
# one commentViewModel when getting a replies to a comment
|
||||
cvm = cvm["commentViewModel"] if cvm["commentViewModel"]?
|
||||
|
||||
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
|
||||
|
||||
json.field "authorIsChannelOwner", comment_author["isCreator"].as_bool
|
||||
json.field "isSponsor", (comment_author["sponsorBadgeUrl"]? != 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
|
||||
|
||||
comment_toolbar = comment_mutation.dig("payload", "commentEntityPayload", "toolbar")
|
||||
json.field "likeCount", short_text_to_number(comment_toolbar["likeCountNotliked"].as_s)
|
||||
reply_count = short_text_to_number(comment_toolbar["replyCount"]?.try &.as_s || "0")
|
||||
|
||||
if heart_state = toolbar_mutation.dig?("payload", "engagementToolbarStateEntityPayload", "heartState")
|
||||
if heart_state.as_s == "TOOLBAR_HEART_STATE_HEARTED"
|
||||
json.field "creatorHeart" do
|
||||
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
|
||||
|
||||
published_text = comment_mutation.dig?("payload", "commentEntityPayload", "properties", "publishedTime").try &.as_s
|
||||
end
|
||||
|
||||
json.field "isPinned", (cvm.dig?("pinnedText") != nil)
|
||||
json.field "commentId", cvm["commentId"]
|
||||
else
|
||||
node_comment = node["commentRenderer"]
|
||||
end
|
||||
if node["comment"]?
|
||||
node_comment = node["comment"]["commentRenderer"]
|
||||
else
|
||||
node_comment = node["commentRenderer"]
|
||||
end
|
||||
json.field "commentId", node_comment["commentId"]
|
||||
html_content = node_comment["contentText"]?.try { |t| parse_content(t, id) }
|
||||
|
||||
content_html = node_comment["contentText"]?.try { |t| parse_content(t, id) } || ""
|
||||
author = node_comment["authorText"]?.try &.["simpleText"]? || ""
|
||||
json.field "verified", (node_comment["authorCommentBadge"]? != nil)
|
||||
|
||||
json.field "verified", (node_comment["authorCommentBadge"]? != nil)
|
||||
|
||||
json.field "author", author
|
||||
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"]
|
||||
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
|
||||
|
||||
if node_comment["authorEndpoint"]?
|
||||
json.field "authorId", node_comment["authorEndpoint"]["browseEndpoint"]["browseId"]
|
||||
json.field "authorUrl", node_comment["authorEndpoint"]["browseEndpoint"]["canonicalBaseUrl"]
|
||||
else
|
||||
json.field "authorId", ""
|
||||
json.field "authorUrl", ""
|
||||
end
|
||||
|
||||
json.field "authorIsChannelOwner", node_comment["authorIsChannelOwner"]
|
||||
json.field "isPinned", (node_comment["pinnedCommentBadge"]? != nil)
|
||||
published_text = node_comment["publishedTimeText"]["runs"][0]["text"].as_s
|
||||
|
||||
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
|
||||
|
||||
reply_count = node_comment["replyCount"]?
|
||||
end
|
||||
|
||||
if node_comment["authorEndpoint"]?
|
||||
json.field "authorId", node_comment["authorEndpoint"]["browseEndpoint"]["browseId"]
|
||||
json.field "authorUrl", node_comment["authorEndpoint"]["browseEndpoint"]["canonicalBaseUrl"]
|
||||
else
|
||||
json.field "authorId", ""
|
||||
json.field "authorUrl", ""
|
||||
end
|
||||
|
||||
published_text = node_comment["publishedTimeText"]["runs"][0]["text"].as_s
|
||||
published = decode_date(published_text.rchop(" (edited)"))
|
||||
|
||||
if published_text.includes?(" (edited)")
|
||||
json.field "isEdited", true
|
||||
else
|
||||
json.field "isEdited", false
|
||||
end
|
||||
|
||||
content_html = html_content || ""
|
||||
json.field "content", html_to_content(content_html)
|
||||
json.field "contentHtml", content_html
|
||||
|
||||
json.field "isPinned", (node_comment["pinnedCommentBadge"]? != nil)
|
||||
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
|
||||
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"]
|
||||
|
||||
json.field "likeCount", comment_action_buttons_renderer["likeButton"]["toggleButtonRenderer"]["accessibilityData"]["accessibilityData"]["label"].as_s.scan(/\d/).map(&.[0]).join.to_i
|
||||
json.field "commentId", node_comment["commentId"]
|
||||
json.field "authorIsChannelOwner", node_comment["authorIsChannelOwner"]
|
||||
|
||||
if comment_action_buttons_renderer["creatorHeart"]?
|
||||
hearth_data = comment_action_buttons_renderer["creatorHeart"]["creatorHeartRenderer"]["creatorThumbnail"]
|
||||
json.field "creatorHeart" do
|
||||
json.object do
|
||||
json.field "creatorThumbnail", hearth_data["thumbnails"][-1]["url"]
|
||||
json.field "creatorName", hearth_data["accessibility"]["accessibilityData"]["label"]
|
||||
end
|
||||
if published_text != nil
|
||||
published_text = published_text.to_s
|
||||
if published_text.includes?(" (edited)")
|
||||
json.field "isEdited", true
|
||||
published = decode_date(published_text.rchop(" (edited)"))
|
||||
else
|
||||
json.field "isEdited", false
|
||||
published = decode_date(published_text)
|
||||
end
|
||||
|
||||
json.field "published", published.to_unix
|
||||
json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale))
|
||||
end
|
||||
|
||||
if node_replies && !response["commentRepliesContinuation"]?
|
||||
|
@ -210,7 +277,7 @@ module Invidious::Comments
|
|||
|
||||
json.field "replies" do
|
||||
json.object do
|
||||
json.field "replyCount", node_comment["replyCount"]? || 1
|
||||
json.field "replyCount", reply_count || 1
|
||||
json.field "continuation", continuation
|
||||
end
|
||||
end
|
||||
|
@ -236,7 +303,6 @@ module Invidious::Comments
|
|||
if format == "html"
|
||||
response = JSON.parse(response)
|
||||
content_html = Frontend::Comments.template_youtube(response, locale, thin_mode)
|
||||
|
||||
response = JSON.build do |json|
|
||||
json.object do
|
||||
json.field "contentHtml", content_html
|
||||
|
|
|
@ -394,7 +394,7 @@ module Invidious::Routes::API::V1::Channels
|
|||
else
|
||||
comments = YoutubeAPI.browse(continuation: continuation)
|
||||
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
|
||||
|
||||
def self.channels(env)
|
||||
|
|
|
@ -231,7 +231,7 @@ module Invidious::Routes::Channels
|
|||
|
||||
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"]
|
||||
comment_html = JSON.parse(Comments.parse_youtube(id, comments, "html", locale, thin_mode, is_post: true))["contentHtml"]
|
||||
end
|
||||
templated "post"
|
||||
end
|
||||
|
|
|
@ -7,7 +7,19 @@ private def copy_string(str : String::Builder, iter : Iterator, count : Int) : I
|
|||
cp = iter.next
|
||||
break if cp.is_a?(Iterator::Stop)
|
||||
|
||||
str << cp.chr
|
||||
if cp == 0x26 # Ampersand (&)
|
||||
str << "&"
|
||||
elsif cp == 0x27 # Single quote (')
|
||||
str << "'"
|
||||
elsif cp == 0x22 # Double quote (")
|
||||
str << """
|
||||
elsif cp == 0x3C # Less-than (<)
|
||||
str << "<"
|
||||
elsif cp == 0x3E # Greater than (>)
|
||||
str << ">"
|
||||
else
|
||||
str << cp.chr
|
||||
end
|
||||
|
||||
# A codepoint from the SMP counts twice
|
||||
copied += 1 if cp > 0xFFFF
|
||||
|
|
Loading…
Reference in a new issue