mirror of
https://github.com/iv-org/invidious.git
synced 2026-09-06 00:52:45 -05:00
Fix CodeRabbit review findings
- Set description_node to nil for absent interactiveTabbedHeaderRenderer so description_node.as_s does not TypeCastError (PR #5984 finding) - Validate byline browseId as non-empty string before selecting channel run - Token-aware view-count/relative-date metadata matching in lockupViewModel parser prevents author rows like 'Chicago' from being misread - linkify_hashtags accepts raw title, HTML-escapes internally, preventing false /hashtag/39 links from escaped apostrophes; template passes raw title - Add regression tests for hashtag apostrophe handling
This commit is contained in:
parent
1832a12d29
commit
ee3711dc8b
@ -43,4 +43,17 @@ Spectator.describe "Utils" do
|
||||
expect(decode_date("8 years ago")).to be_close(Time.utc - 8.years, 500.milliseconds)
|
||||
end
|
||||
end
|
||||
|
||||
describe "linkify_hashtags" do
|
||||
it "escapes apostrophes without creating false hashtag links" do
|
||||
result = linkify_hashtags("Rock 'n' Roll `#music`")
|
||||
expect(result).to_not contain("/hashtag/39")
|
||||
expect(result).to contain("'")
|
||||
expect(result).to contain("<a href=\"/hashtag/music\">#music</a>")
|
||||
end
|
||||
|
||||
it "links a plain hashtag" do
|
||||
expect(linkify_hashtags("hello #music")).to eq("hello <a href=\"/hashtag/music\">#music</a>")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -75,7 +75,7 @@ def get_about_info(ucid) : AboutChannel
|
||||
author_url = "https://www.youtube.com/channel/#{ucid}"
|
||||
author_thumbnail = phr_vm.try &.dig?("animatedImage", "contentPreviewImageViewModel", "image", "sources", 0, "url").try &.as_s || ""
|
||||
banner = nil
|
||||
description_node = JSON.parse(%({"simpleText": ""}))
|
||||
description_node = nil
|
||||
tags = [] of String
|
||||
else
|
||||
author = ithr["title"]["simpleText"].as_s
|
||||
|
||||
@ -386,15 +386,15 @@ def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String)
|
||||
return text
|
||||
end
|
||||
|
||||
def linkify_hashtags(title : String) : String
|
||||
def linkify_hashtags(raw_title : String) : String
|
||||
String.build do |str|
|
||||
pos = 0
|
||||
title.scan(/#([a-zA-Z0-9_]+)/) do |match|
|
||||
str << title[pos...match.begin(0)]
|
||||
str << %(<a href="/hashtag/#{URI.encode_path(match[1])}">#{match[0]}</a>)
|
||||
raw_title.scan(/#([a-zA-Z0-9_]+)/) do |match|
|
||||
str << HTML.escape(raw_title[pos...match.begin(0)])
|
||||
str << %(<a href="/hashtag/#{URI.encode_path(match[1])}">#{HTML.escape(match[0])}</a>)
|
||||
pos = match.end(0)
|
||||
end
|
||||
str << title[pos..]
|
||||
str << HTML.escape(raw_title[pos..])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@ -20,14 +20,16 @@ module Invidious::Videos::Parser
|
||||
end
|
||||
|
||||
# Both have "short", so the "long" option shouldn't be required
|
||||
# Use the first run that has a valid browse endpoint.
|
||||
# Use the first run that has a non-empty string browseId.
|
||||
# For collaboration videos, the first run is a plain text summary
|
||||
# without a link, while subsequent runs contain the channel links.
|
||||
byline_runs = (related["shortBylineText"]? || related["longBylineText"]?)
|
||||
.try &.dig?("runs").try &.as_a
|
||||
|
||||
channel_info = byline_runs.try &.find do |run|
|
||||
run.dig?("navigationEndpoint", "browseEndpoint", "browseId")
|
||||
browse_id = run.dig?("navigationEndpoint", "browseEndpoint", "browseId")
|
||||
text_id = browse_id.try &.as_s?
|
||||
!text_id.nil? && !text_id.empty?
|
||||
end || byline_runs.try &.[0]?
|
||||
|
||||
author = channel_info.try &.dig?("text")
|
||||
|
||||
@ -94,7 +94,7 @@ we're going to need to do it here in order to allow for translations.
|
||||
<div class="h-box">
|
||||
<h1>
|
||||
<% if video.title.matches?(/#\w/) %>
|
||||
<%= linkify_hashtags(title) %>
|
||||
<%= linkify_hashtags(video.title) %>
|
||||
<% else %>
|
||||
<%= title %>
|
||||
<% end %>
|
||||
|
||||
@ -642,6 +642,13 @@ private module Parsers
|
||||
extend self
|
||||
include BaseParser
|
||||
|
||||
private VIEW_COUNT_TOKEN = /\d+\s+views/
|
||||
private RELATIVE_DATE_TOKEN = /\d+\s+(second|minute|hour|day|week|month|year)s?\s+ago/
|
||||
|
||||
private def metadata_part_text(item : JSON::Any) : String?
|
||||
item.dig?("text", "content").try &.as_s
|
||||
end
|
||||
|
||||
def process(item : JSON::Any, author_fallback : AuthorFallback)
|
||||
if item_contents = item["lockupViewModel"]?
|
||||
return self.parse(item_contents, author_fallback)
|
||||
@ -664,14 +671,16 @@ private module Parsers
|
||||
# Contains the views of the video and the published time of the video.
|
||||
# For collaboration videos, the first row contains the author names
|
||||
# instead, so we scan all rows for the one with view/publish info.
|
||||
# Token-aware matching prevents author rows (e.g. "Chicago") from
|
||||
# being mistaken for view-count or relative-date metadata.
|
||||
metadata_parts = metadata.dig?("metadata", "contentMetadataViewModel", "metadataRows")
|
||||
.try &.as_a
|
||||
.compact_map { |row| row.dig?("metadataParts").try &.as_a }
|
||||
.find { |parts| parts.any? { |item| item.dig?("text", "content").try &.as_s.includes?("views") || item.dig?("text", "content").try &.as_s.includes?("ago") } }
|
||||
.find { |parts| parts.any? { |item| metadata_part_text(item).try { |t| t.matches?(VIEW_COUNT_TOKEN) || t.matches?(RELATIVE_DATE_TOKEN) } } }
|
||||
|
||||
view_count_text = metadata_parts.try &.find { |item| item["icon"]?.nil? && item.dig?("text", "content").try &.as_s.includes?("views") }
|
||||
view_count_text = metadata_parts.try &.find { |item| item["icon"]?.nil? && metadata_part_text(item).try(&.matches?(VIEW_COUNT_TOKEN)) }
|
||||
.try &.dig("text", "content").as_s
|
||||
published = metadata_parts.try &.find { |item| item["icon"]?.nil? && item.dig?("text", "content").try &.as_s.includes?("ago") }
|
||||
published = metadata_parts.try &.find { |item| item["icon"]?.nil? && metadata_part_text(item).try(&.matches?(RELATIVE_DATE_TOKEN)) }
|
||||
.try { |item| decode_date(item.dig("text", "content").as_s) } || Time.local
|
||||
|
||||
view_count = short_text_to_number(view_count_text || "0")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user