summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/invidious.cr1
-rw-r--r--src/invidious/channels/about.cr4
-rw-r--r--src/invidious/channels/community.cr4
-rw-r--r--src/invidious/comments.cr14
-rw-r--r--src/invidious/hashtag.cr44
-rw-r--r--src/invidious/routes/feeds.cr2
-rw-r--r--src/invidious/routes/search.cr31
-rw-r--r--src/invidious/videos.cr25
-rw-r--r--src/invidious/views/hashtag.ecr39
-rw-r--r--src/invidious/views/licenses.ecr14
-rw-r--r--src/invidious/views/template.ecr1
-rw-r--r--src/invidious/views/watch.ecr38
-rw-r--r--src/invidious/yt_backend/extractors.cr62
-rw-r--r--src/invidious/yt_backend/extractors_utils.cr41
14 files changed, 257 insertions, 63 deletions
diff --git a/src/invidious.cr b/src/invidious.cr
index dd240852..4952b365 100644
--- a/src/invidious.cr
+++ b/src/invidious.cr
@@ -385,6 +385,7 @@ end
Invidious::Routing.get "/opensearch.xml", Invidious::Routes::Search, :opensearch
Invidious::Routing.get "/results", Invidious::Routes::Search, :results
Invidious::Routing.get "/search", Invidious::Routes::Search, :search
+ Invidious::Routing.get "/hashtag/:hashtag", Invidious::Routes::Search, :hashtag
# User routes
define_user_routes()
diff --git a/src/invidious/channels/about.cr b/src/invidious/channels/about.cr
index 31b19bbe..c2027f90 100644
--- a/src/invidious/channels/about.cr
+++ b/src/invidious/channels/about.cr
@@ -66,6 +66,7 @@ def get_about_info(ucid, locale) : AboutChannel
author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s
author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s
author_thumbnail = initdata["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].as_s
+ author_verified = has_verified_badge?(initdata.dig?("header", "c4TabbedHeaderRenderer", "badges"))
ucid = initdata["metadata"]["channelMetadataRenderer"]["externalId"].as_s
@@ -76,9 +77,6 @@ def get_about_info(ucid, locale) : AboutChannel
# if banner.includes? "channels/c4/default_banner"
# banner = nil
# end
- # author_verified_badges = initdata["header"]?.try &.["c4TabbedHeaderRenderer"]?.try &.["badges"]?
- author_verified_badge = initdata["header"].dig?("c4TabbedHeaderRenderer", "badges", 0, "metadataBadgeRenderer", "tooltip")
- author_verified = (author_verified_badge && author_verified_badge == "Verified")
description_node = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]?
diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr
index ebef0edb..2a2c74aa 100644
--- a/src/invidious/channels/community.cr
+++ b/src/invidious/channels/community.cr
@@ -13,13 +13,11 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode)
if !continuation || continuation.empty?
initial_data = extract_initial_data(response.body)
- body = initial_data["contents"]?.try &.["twoColumnBrowseResultsRenderer"]["tabs"].as_a.select { |tab| tab["tabRenderer"]?.try &.["selected"].as_bool.== true }[0]?
+ body = extract_selected_tab(initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"])["content"]["sectionListRenderer"]["contents"][0]["itemSectionRenderer"]
if !body
raise InfoException.new("Could not extract community tab.")
end
-
- body = body["tabRenderer"]["content"]["sectionListRenderer"]["contents"][0]["itemSectionRenderer"]
else
continuation = produce_channel_community_continuation(ucid, continuation)
diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr
index f2e63265..5112ad3d 100644
--- a/src/invidious/comments.cr
+++ b/src/invidious/comments.cr
@@ -481,7 +481,7 @@ def template_reddit_comments(root, locale)
html << <<-END_HTML
<p>
- <a href="javascript:void(0)" data-onclick="toggle_parent">[ - ]</a>
+ <a href="javascript:void(0)" data-onclick="toggle_parent">[ − ]</a>
<b><a href="https://www.reddit.com/user/#{child.author}">#{child.author}</a></b>
#{translate_count(locale, "comments_points_count", child.score, NumberFormatting::Separator)}
<span title="#{child.created_utc.to_s(translate(locale, "%a %B %-d %T %Y UTC"))}">#{translate(locale, "`x` ago", recode_date(child.created_utc, locale))}</span>
@@ -500,6 +500,12 @@ def template_reddit_comments(root, locale)
end
def replace_links(html)
+ # Check if the document is empty
+ # Prevents edge-case bug with Reddit comments, see issue #3115
+ if html.nil? || html.empty?
+ return html
+ end
+
html = XML.parse_html(html)
html.xpath_nodes(%q(//a)).each do |anchor|
@@ -541,6 +547,12 @@ def replace_links(html)
end
def fill_links(html, scheme, host)
+ # Check if the document is empty
+ # Prevents edge-case bug with Reddit comments, see issue #3115
+ if html.nil? || html.empty?
+ return html
+ end
+
html = XML.parse_html(html)
html.xpath_nodes("//a").each do |match|
diff --git a/src/invidious/hashtag.cr b/src/invidious/hashtag.cr
new file mode 100644
index 00000000..afe31a36
--- /dev/null
+++ b/src/invidious/hashtag.cr
@@ -0,0 +1,44 @@
+module Invidious::Hashtag
+ extend self
+
+ def fetch(hashtag : String, page : Int, region : String? = nil) : Array(SearchItem)
+ cursor = (page - 1) * 60
+ ctoken = generate_continuation(hashtag, cursor)
+
+ client_config = YoutubeAPI::ClientConfig.new(region: region)
+ response = YoutubeAPI.browse(continuation: ctoken, client_config: client_config)
+
+ return extract_items(response)
+ end
+
+ def generate_continuation(hashtag : String, cursor : Int)
+ object = {
+ "80226972:embedded" => {
+ "2:string" => "FEhashtag",
+ "3:base64" => {
+ "1:varint" => cursor.to_i64,
+ },
+ "7:base64" => {
+ "325477796:embedded" => {
+ "1:embedded" => {
+ "2:0:embedded" => {
+ "2:string" => '#' + hashtag,
+ "4:varint" => 0_i64,
+ "11:string" => "",
+ },
+ "4:string" => "browse-feedFEhashtag",
+ },
+ "2:string" => hashtag,
+ },
+ },
+ },
+ }
+
+ continuation = 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) }
+
+ return continuation
+ end
+end
diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr
index 31120ecb..44a87175 100644
--- a/src/invidious/routes/feeds.cr
+++ b/src/invidious/routes/feeds.cr
@@ -184,7 +184,7 @@ module Invidious::Routes::Feeds
paid: false,
premium: false,
premiere_timestamp: nil,
- author_verified: false, # ¯\_(ツ)_/¯
+ author_verified: false,
})
end
diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr
index e60d0081..6f8bffea 100644
--- a/src/invidious/routes/search.cr
+++ b/src/invidious/routes/search.cr
@@ -63,4 +63,35 @@ module Invidious::Routes::Search
templated "search"
end
end
+
+ def self.hashtag(env : HTTP::Server::Context)
+ locale = env.get("preferences").as(Preferences).locale
+
+ hashtag = env.params.url["hashtag"]?
+ if hashtag.nil? || hashtag.empty?
+ return error_template(400, "Invalid request")
+ end
+
+ page = env.params.query["page"]?
+ if page.nil?
+ page = 1
+ else
+ page = Math.max(1, page.to_i)
+ env.params.query.delete_all("page")
+ end
+
+ begin
+ videos = Invidious::Hashtag.fetch(hashtag, page)
+ rescue ex
+ return error_template(500, ex)
+ end
+
+ params = env.params.query.empty? ? "" : "&#{env.params.query}"
+
+ hashtag_encoded = URI.encode_www_form(hashtag, space_to_plus: false)
+ url_prev_page = "/hashtag/#{hashtag_encoded}?page=#{page - 1}#{params}"
+ url_next_page = "/hashtag/#{hashtag_encoded}?page=#{page + 1}#{params}"
+
+ templated "hashtag"
+ end
end
diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr
index 20204d81..1a547ee0 100644
--- a/src/invidious/videos.cr
+++ b/src/invidious/videos.cr
@@ -853,6 +853,7 @@ end
# the same 11 first entries as the compact rendered.
#
# TODO: "compactRadioRenderer" (Mix) and
+# TODO: Use a proper struct/class instead of a hacky JSON object
def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
return nil if !related["videoId"]?
@@ -868,11 +869,7 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
.try &.dig?("runs", 0)
author = channel_info.try &.dig?("text")
- author_verified_badge = related["ownerBadges"]?.try do |badges_array|
- badges_array.as_a.find(&.dig("metadataBadgeRenderer", "tooltip").as_s.== "Verified")
- end
-
- author_verified = (author_verified_badge && author_verified_badge.size > 0).to_s
+ author_verified = has_verified_badge?(related["ownerBadges"]?).to_s
ucid = channel_info.try { |ci| HelperExtractors.get_browse_id(ci) }
@@ -1089,17 +1086,19 @@ def extract_video_info(video_id : String, proxy_region : String? = nil, context_
# Author infos
- author_info = video_secondary_renderer.try &.dig?("owner", "videoOwnerRenderer")
- author_thumbnail = author_info.try &.dig?("thumbnail", "thumbnails", 0, "url")
+ if author_info = video_secondary_renderer.try &.dig?("owner", "videoOwnerRenderer")
+ author_thumbnail = author_info.dig?("thumbnail", "thumbnails", 0, "url")
+ params["authorThumbnail"] = JSON::Any.new(author_thumbnail.try &.as_s || "")
- author_verified_badge = author_info.try &.dig?("badges", 0, "metadataBadgeRenderer", "tooltip")
- author_verified = (!author_verified_badge.nil? && author_verified_badge == "Verified")
- params["authorVerified"] = JSON::Any.new(author_verified)
+ author_verified = has_verified_badge?(author_info["badges"]?)
+ params["authorVerified"] = JSON::Any.new(author_verified)
- params["authorThumbnail"] = JSON::Any.new(author_thumbnail.try &.as_s || "")
+ subs_text = author_info["subscriberCountText"]?
+ .try { |t| t["simpleText"]? || t.dig?("runs", 0, "text") }
+ .try &.as_s.split(" ", 2)[0]
- params["subCountText"] = JSON::Any.new(author_info.try &.["subscriberCountText"]?
- .try { |t| t["simpleText"]? || t.dig?("runs", 0, "text") }.try &.as_s.split(" ", 2)[0] || "-")
+ params["subCountText"] = JSON::Any.new(subs_text || "-")
+ end
# Return data
diff --git a/src/invidious/views/hashtag.ecr b/src/invidious/views/hashtag.ecr
new file mode 100644
index 00000000..0ecfe832
--- /dev/null
+++ b/src/invidious/views/hashtag.ecr
@@ -0,0 +1,39 @@
+<% content_for "header" do %>
+<title><%= HTML.escape(hashtag) %> - Invidious</title>
+<% end %>
+
+<hr/>
+
+<div class="pure-g h-box v-box">
+ <div class="pure-u-1 pure-u-lg-1-5">
+ <%- if page > 1 -%>
+ <a href="<%= url_prev_page %>"><%= translate(locale, "Previous page") %></a>
+ <%- end -%>
+ </div>
+ <div class="pure-u-1 pure-u-lg-3-5"></div>
+ <div class="pure-u-1 pure-u-lg-1-5" style="text-align:right">
+ <%- if videos.size >= 60 -%>
+ <a href="<%= url_next_page %>"><%= translate(locale, "Next page") %></a>
+ <%- end -%>
+ </div>
+</div>
+
+<div class="pure-g">
+ <%- videos.each do |item| -%>
+ <%= rendered "components/item" %>
+ <%- end -%>
+</div>
+
+<div class="pure-g h-box">
+ <div class="pure-u-1 pure-u-lg-1-5">
+ <%- if page > 1 -%>
+ <a href="<%= url_prev_page %>"><%= translate(locale, "Previous page") %></a>
+ <%- end -%>
+ </div>
+ <div class="pure-u-1 pure-u-lg-3-5"></div>
+ <div class="pure-u-1 pure-u-lg-1-5" style="text-align:right">
+ <%- if videos.size >= 60 -%>
+ <a href="<%= url_next_page %>"><%= translate(locale, "Next page") %></a>
+ <%- end -%>
+ </div>
+</div>
diff --git a/src/invidious/views/licenses.ecr b/src/invidious/views/licenses.ecr
index 861913d0..25b24ed4 100644
--- a/src/invidious/views/licenses.ecr
+++ b/src/invidious/views/licenses.ecr
@@ -11,6 +11,20 @@
<table id="jslicense-labels1">
<tr>
<td>
+ <a href="/js/_helpers.js?v=<%= ASSET_COMMIT %>">_helpers.js</a>
+ </td>
+
+ <td>
+ <a href="https://www.gnu.org/licenses/agpl-3.0.html">AGPL-3.0</a>
+ </td>
+
+ <td>
+ <a href="/js/_helpers.js?v=<%= ASSET_COMMIT %>"><%= translate(locale, "source") %></a>
+ </td>
+ </tr>
+
+ <tr>
+ <td>
<a href="/js/community.js?v=<%= ASSET_COMMIT %>">community.js</a>
</td>
diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr
index bd908dd6..4e2b29f0 100644
--- a/src/invidious/views/template.ecr
+++ b/src/invidious/views/template.ecr
@@ -17,6 +17,7 @@
<link rel="stylesheet" href="/css/grids-responsive-min.css?v=<%= ASSET_COMMIT %>">
<link rel="stylesheet" href="/css/ionicons.min.css?v=<%= ASSET_COMMIT %>">
<link rel="stylesheet" href="/css/default.css?v=<%= ASSET_COMMIT %>">
+ <script src="/js/_helpers.js?v=<%= ASSET_COMMIT %>"></script>
</head>
<%
diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr
index 8b6eb903..d1fdcce2 100644
--- a/src/invidious/views/watch.ecr
+++ b/src/invidious/views/watch.ecr
@@ -173,7 +173,7 @@ we're going to need to do it here in order to allow for translations.
<p id="views"><i class="icon ion-ios-eye"></i> <%= number_with_separator(video.views) %></p>
<p id="likes"><i class="icon ion-ios-thumbs-up"></i> <%= number_with_separator(video.likes) %></p>
- <p id="dislikes"><i class="icon ion-ios-thumbs-down"></i> <%= number_with_separator(video.dislikes) %></p>
+ <p id="dislikes"></p>
<p id="genre"><%= translate(locale, "Genre: ") %>
<% if !video.genre_url %>
<%= video.genre %>
@@ -186,7 +186,7 @@ we're going to need to do it here in order to allow for translations.
<% end %>
<p id="family_friendly"><%= translate(locale, "Family friendly? ") %><%= translate_bool(locale, video.is_family_friendly) %></p>
<p id="wilson"><%= translate(locale, "Wilson score: ") %><%= video.wilson_score %></p>
- <p id="rating"><%= translate(locale, "Rating: ") %><%= video.average_rating %> / 5</p>
+ <p id="rating"></p>
<p id="engagement"><%= translate(locale, "Engagement: ") %><%= video.engagement %>%</p>
<% if video.allowed_regions.size != REGIONS.size %>
<p id="allowed_regions">
@@ -278,24 +278,24 @@ we're going to need to do it here in order to allow for translations.
</div>
<% end %>
<p style="width:100%"><%= rv["title"] %></p>
- <h5 class="pure-g">
- <div class="pure-u-14-24">
- <% if rv["ucid"]? %>
- <b style="width:100%"><a href="/channel/<%= rv["ucid"] %>"><%= rv["author"]? %><% if rv["author_verified"]? == "true" %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %></a></b>
- <% else %>
- <b style="width:100%"><%= rv["author"]? %><% if rv["author_verified"]? == "true" %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %></b>
- <% end %>
- </div>
-
- <div class="pure-u-10-24" style="text-align:right">
- <b class="width:100%"><%=
- views = rv["view_count"]?.try &.to_i?
- views ||= rv["view_count_short"]?.try { |x| short_text_to_number(x) }
- translate_count(locale, "generic_views_count", views || 0, NumberFormatting::Short)
- %></b>
- </div>
- </h5>
</a>
+ <h5 class="pure-g">
+ <div class="pure-u-14-24">
+ <% if rv["ucid"]? %>
+ <b style="width:100%"><a href="/channel/<%= rv["ucid"] %>"><%= rv["author"]? %><% if rv["author_verified"]? == "true" %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %></a></b>
+ <% else %>
+ <b style="width:100%"><%= rv["author"]? %><% if rv["author_verified"]? == "true" %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %></b>
+ <% end %>
+ </div>
+
+ <div class="pure-u-10-24" style="text-align:right">
+ <b class="width:100%"><%=
+ views = rv["view_count"]?.try &.to_i?
+ views ||= rv["view_count_short"]?.try { |x| short_text_to_number(x) }
+ translate_count(locale, "generic_views_count", views || 0, NumberFormatting::Short)
+ %></b>
+ </div>
+ </h5>
<% end %>
<% end %>
</div>
diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr
index a2ec7d59..b9609eb9 100644
--- a/src/invidious/yt_backend/extractors.cr
+++ b/src/invidious/yt_backend/extractors.cr
@@ -1,3 +1,5 @@
+require "../helpers/serialized_yt_data"
+
# This file contains helper methods to parse the Youtube API json data into
# neat little packages we can use
@@ -14,6 +16,7 @@ private ITEM_PARSERS = {
Parsers::GridPlaylistRendererParser,
Parsers::PlaylistRendererParser,
Parsers::CategoryRendererParser,
+ Parsers::RichItemRendererParser,
}
record AuthorFallback, name : String, id : String
@@ -57,6 +60,8 @@ private module Parsers
author_id = author_fallback.id
end
+ author_verified = has_verified_badge?(item_contents["ownerBadges"]?)
+
# For live videos (and possibly recently premiered videos) there is no published information.
# Instead, in its place is the amount of people currently watching. This behavior should be replicated
# on Invidious once all features of livestreams are supported. On an unrelated note, defaulting to the current
@@ -102,11 +107,7 @@ private module Parsers
premium = false
premiere_timestamp = item_contents.dig?("upcomingEventData", "startTime").try { |t| Time.unix(t.as_s.to_i64) }
- author_verified_badge = item_contents["ownerBadges"]?.try do |badges_array|
- badges_array.as_a.find(&.dig("metadataBadgeRenderer", "tooltip").as_s.== "Verified")
- end
- author_verified = (author_verified_badge && author_verified_badge.size > 0)
item_contents["badges"]?.try &.as_a.each do |badge|
b = badge["metadataBadgeRenderer"]
case b["label"].as_s
@@ -133,7 +134,7 @@ private module Parsers
live_now: live_now,
premium: premium,
premiere_timestamp: premiere_timestamp,
- author_verified: author_verified || false,
+ author_verified: author_verified,
})
end
@@ -161,12 +162,9 @@ private module Parsers
private def self.parse(item_contents, author_fallback)
author = extract_text(item_contents["title"]) || author_fallback.name
author_id = item_contents["channelId"]?.try &.as_s || author_fallback.id
- author_verified_badge = item_contents["ownerBadges"]?.try do |badges_array|
- badges_array.as_a.find(&.dig("metadataBadgeRenderer", "tooltip").as_s.== "Verified")
- end
-
- author_verified = (author_verified_badge && author_verified_badge.size > 0)
+ author_verified = has_verified_badge?(item_contents["ownerBadges"]?)
author_thumbnail = HelperExtractors.get_thumbnails(item_contents)
+
# When public subscriber count is disabled, the subscriberCountText isn't sent by InnerTube.
# Always simpleText
# TODO change default value to nil
@@ -188,7 +186,7 @@ private module Parsers
video_count: video_count,
description_html: description_html,
auto_generated: auto_generated,
- author_verified: author_verified || false,
+ author_verified: author_verified,
})
end
@@ -216,11 +214,9 @@ private module Parsers
private def self.parse(item_contents, author_fallback)
title = extract_text(item_contents["title"]) || ""
plid = item_contents["playlistId"]?.try &.as_s || ""
- author_verified_badge = item_contents["ownerBadges"]?.try do |badges_array|
- badges_array.as_a.find(&.dig("metadataBadgeRenderer", "tooltip").as_s.== "Verified")
- end
- author_verified = (author_verified_badge && author_verified_badge.size > 0)
+ author_verified = has_verified_badge?(item_contents["ownerBadges"]?)
+
video_count = HelperExtractors.get_video_count(item_contents)
playlist_thumbnail = HelperExtractors.get_thumbnails(item_contents)
@@ -232,7 +228,7 @@ private module Parsers
video_count: video_count,
videos: [] of SearchPlaylistVideo,
thumbnail: playlist_thumbnail,
- author_verified: author_verified || false,
+ author_verified: author_verified,
})
end
@@ -266,11 +262,8 @@ private module Parsers
author_info = item_contents.dig?("shortBylineText", "runs", 0)
author = author_info.try &.["text"].as_s || author_fallback.name
author_id = author_info.try { |x| HelperExtractors.get_browse_id(x) } || author_fallback.id
- author_verified_badge = item_contents["ownerBadges"]?.try do |badges_array|
- badges_array.as_a.find(&.dig("metadataBadgeRenderer", "tooltip").as_s.== "Verified")
- end
+ author_verified = has_verified_badge?(item_contents["ownerBadges"]?)
- author_verified = (author_verified_badge && author_verified_badge.size > 0)
videos = item_contents["videos"]?.try &.as_a.map do |v|
v = v["childVideoRenderer"]
v_title = v.dig?("title", "simpleText").try &.as_s || ""
@@ -293,7 +286,7 @@ private module Parsers
video_count: video_count,
videos: videos,
thumbnail: playlist_thumbnail,
- author_verified: author_verified || false,
+ author_verified: author_verified,
})
end
@@ -374,6 +367,29 @@ private module Parsers
return {{@type.name}}
end
end
+
+ # Parses an InnerTube richItemRenderer into a SearchVideo.
+ # Returns nil when the given object isn't a shelfRenderer
+ #
+ # A richItemRenderer seems to be a simple wrapper for a videoRenderer, used
+ # by the result page for hashtags. It is located inside a continuationItems
+ # container.
+ #
+ module RichItemRendererParser
+ def self.process(item : JSON::Any, author_fallback : AuthorFallback)
+ if item_contents = item.dig?("richItemRenderer", "content")
+ return self.parse(item_contents, author_fallback)
+ end
+ end
+
+ private def self.parse(item_contents, author_fallback)
+ return VideoRendererParser.process(item_contents, author_fallback)
+ end
+
+ def self.parser_name
+ return {{@type.name}}
+ end
+ end
end
# The following are the extractors for extracting an array of items from
@@ -401,7 +417,7 @@ private module Extractors
# {"tabRenderer": {
# "endpoint": {...}
# "title": "Playlists",
- # "selected": true,
+ # "selected": true, # Is nil unless tab is selected
# "content": {...},
# ...
# }}
@@ -501,6 +517,8 @@ private module Extractors
self.extract(target)
elsif target = initial_data["appendContinuationItemsAction"]?
self.extract(target)
+ elsif target = initial_data["reloadContinuationItemsCommand"]?
+ self.extract(target)
end
end
diff --git a/src/invidious/yt_backend/extractors_utils.cr b/src/invidious/yt_backend/extractors_utils.cr
index add5f488..f8245160 100644
--- a/src/invidious/yt_backend/extractors_utils.cr
+++ b/src/invidious/yt_backend/extractors_utils.cr
@@ -29,6 +29,45 @@ def extract_text(item : JSON::Any?) : String?
end
end
+# Check if an "ownerBadges" or a "badges" element contains a verified badge.
+# There is currently two known types of verified badges:
+#
+# "ownerBadges": [{
+# "metadataBadgeRenderer": {
+# "icon": { "iconType": "CHECK_CIRCLE_THICK" },
+# "style": "BADGE_STYLE_TYPE_VERIFIED",
+# "tooltip": "Verified",
+# "accessibilityData": { "label": "Verified" }
+# }
+# }],
+#
+# "ownerBadges": [{
+# "metadataBadgeRenderer": {
+# "icon": { "iconType": "OFFICIAL_ARTIST_BADGE" },
+# "style": "BADGE_STYLE_TYPE_VERIFIED_ARTIST",
+# "tooltip": "Official Artist Channel",
+# "accessibilityData": { "label": "Official Artist Channel" }
+# }
+# }],
+#
+def has_verified_badge?(badges : JSON::Any?)
+ return false if badges.nil?
+
+ badges.as_a.each do |badge|
+ style = badge.dig("metadataBadgeRenderer", "style").as_s
+
+ return true if style == "BADGE_STYLE_TYPE_VERIFIED"
+ return true if style == "BADGE_STYLE_TYPE_VERIFIED_ARTIST"
+ end
+
+ return false
+rescue ex
+ LOGGER.debug("Unable to parse owner badges. Got exception: #{ex.message}")
+ LOGGER.trace("Owner badges data: #{badges.to_json}")
+
+ return false
+end
+
def extract_videos(initial_data : Hash(String, JSON::Any), author_fallback : String? = nil, author_id_fallback : String? = nil)
extracted = extract_items(initial_data, author_fallback, author_id_fallback)
@@ -45,7 +84,7 @@ end
def extract_selected_tab(tabs)
# Extract the selected tab from the array of tabs Youtube returns
- return selected_target = tabs.as_a.select(&.["tabRenderer"]?.try &.["selected"].as_bool)[0]["tabRenderer"]
+ return selected_target = tabs.as_a.select(&.["tabRenderer"]?.try &.["selected"]?.try &.as_bool)[0]["tabRenderer"]
end
def fetch_continuation_token(items : Array(JSON::Any))