summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorSamantaz Fox <coding@samantaz.fr>2023-08-26 12:11:38 +0200
committerSamantaz Fox <coding@samantaz.fr>2023-08-26 12:11:38 +0200
commita8295b452e73bed8c4943f3a124e5a180f076364 (patch)
tree38eec333203520de9bad112cebd90f60aabbc42a /src
parent95176a8eb40aa896f4642b2464bc7585d64f7d59 (diff)
parent70b80ce8ad5ad9e5eb57a8f2f8e72a2274f8523f (diff)
downloadinvidious-a8295b452e73bed8c4943f3a124e5a180f076364.tar.gz
invidious-a8295b452e73bed8c4943f3a124e5a180f076364.tar.bz2
invidious-a8295b452e73bed8c4943f3a124e5a180f076364.zip
Search: Add hashtag result (#3989)
Diffstat (limited to 'src')
-rw-r--r--src/invidious/helpers/serialized_yt_data.cr21
-rw-r--r--src/invidious/views/components/item.ecr26
-rw-r--r--src/invidious/yt_backend/extractors.cr53
3 files changed, 97 insertions, 3 deletions
diff --git a/src/invidious/helpers/serialized_yt_data.cr b/src/invidious/helpers/serialized_yt_data.cr
index 7c12ad0e..e0bd7279 100644
--- a/src/invidious/helpers/serialized_yt_data.cr
+++ b/src/invidious/helpers/serialized_yt_data.cr
@@ -232,6 +232,25 @@ struct SearchChannel
end
end
+struct SearchHashtag
+ include DB::Serializable
+
+ property title : String
+ property url : String
+ property video_count : Int64
+ property channel_count : Int64
+
+ def to_json(locale : String?, json : JSON::Builder)
+ json.object do
+ json.field "type", "hashtag"
+ json.field "title", self.title
+ json.field "url", self.url
+ json.field "videoCount", self.video_count
+ json.field "channelCount", self.channel_count
+ end
+ end
+end
+
class Category
include DB::Serializable
@@ -274,4 +293,4 @@ struct Continuation
end
end
-alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist | Category
+alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist | SearchHashtag | Category
diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr
index 7ffd2d93..c29ec47b 100644
--- a/src/invidious/views/components/item.ecr
+++ b/src/invidious/views/components/item.ecr
@@ -1,6 +1,6 @@
<%-
thin_mode = env.get("preferences").as(Preferences).thin_mode
- item_watched = !item.is_a?(SearchChannel | SearchPlaylist | InvidiousPlaylist | Category) && env.get?("user").try &.as(User).watched.index(item.id) != nil
+ item_watched = !item.is_a?(SearchChannel | SearchHashtag | SearchPlaylist | InvidiousPlaylist | Category) && env.get?("user").try &.as(User).watched.index(item.id) != nil
author_verified = item.responds_to?(:author_verified) && item.author_verified
-%>
@@ -29,6 +29,30 @@
<p><%= translate_count(locale, "generic_subscribers_count", item.subscriber_count, NumberFormatting::Separator) %></p>
<% if !item.auto_generated %><p><%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %></p><% end %>
<h5><%= item.description_html %></h5>
+ <% when SearchHashtag %>
+ <% if !thin_mode %>
+ <a tabindex="-1" href="<%= item.url %>">
+ <center><img style="width:56.25%" src="/hashtag.svg" alt="" /></center>
+ </a>
+ <%- else -%>
+ <div class="thumbnail-placeholder" style="width:56.25%"></div>
+ <% end %>
+
+ <div class="video-card-row">
+ <div class="flex-left"><a href="<%= item.url %>"><%= HTML.escape(item.title) %></a></div>
+ </div>
+
+ <div class="video-card-row">
+ <%- if item.video_count != 0 -%>
+ <p><%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %></p>
+ <%- end -%>
+ </div>
+
+ <div class="video-card-row">
+ <%- if item.channel_count != 0 -%>
+ <p><%= translate_count(locale, "generic_channels_count", item.channel_count, NumberFormatting::Separator) %></p>
+ <%- end -%>
+ </div>
<% when SearchPlaylist, InvidiousPlaylist %>
<%-
if item.id.starts_with? "RD"
diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr
index 8cf59d50..aaf7772e 100644
--- a/src/invidious/yt_backend/extractors.cr
+++ b/src/invidious/yt_backend/extractors.cr
@@ -11,15 +11,16 @@ private ITEM_CONTAINER_EXTRACTOR = {
}
private ITEM_PARSERS = {
+ Parsers::RichItemRendererParser,
Parsers::VideoRendererParser,
Parsers::ChannelRendererParser,
Parsers::GridPlaylistRendererParser,
Parsers::PlaylistRendererParser,
Parsers::CategoryRendererParser,
- Parsers::RichItemRendererParser,
Parsers::ReelItemRendererParser,
Parsers::ItemSectionRendererParser,
Parsers::ContinuationItemRendererParser,
+ Parsers::HashtagRendererParser,
}
private alias InitialData = Hash(String, JSON::Any)
@@ -210,6 +211,56 @@ private module Parsers
end
end
+ # Parses an Innertube `hashtagTileRenderer` into a `SearchHashtag`.
+ # Returns `nil` when the given object is not a `hashtagTileRenderer`.
+ #
+ # A `hashtagTileRenderer` is a kind of search result.
+ # It can be found when searching for any hashtag (e.g "#hi" or "#shorts")
+ module HashtagRendererParser
+ def self.process(item : JSON::Any, author_fallback : AuthorFallback)
+ if item_contents = item["hashtagTileRenderer"]?
+ return self.parse(item_contents)
+ end
+ end
+
+ private def self.parse(item_contents)
+ title = extract_text(item_contents["hashtag"]).not_nil! # E.g "#hi"
+
+ # E.g "/hashtag/hi"
+ url = item_contents.dig?("onTapCommand", "commandMetadata", "webCommandMetadata", "url").try &.as_s
+ url ||= URI.encode_path("/hashtag/#{title.lchop('#')}")
+
+ video_count_txt = extract_text(item_contents["hashtagVideoCount"]?) # E.g "203K videos"
+ channel_count_txt = extract_text(item_contents["hashtagChannelCount"]?) # E.g "81K channels"
+
+ # Fallback for video/channel counts
+ if channel_count_txt.nil? || video_count_txt.nil?
+ # E.g: "203K videos • 81K channels"
+ info_text = extract_text(item_contents["hashtagInfoText"]?).try &.split(" • ")
+
+ if info_text && info_text.size == 2
+ video_count_txt ||= info_text[0]
+ channel_count_txt ||= info_text[1]
+ end
+ end
+
+ return SearchHashtag.new({
+ title: title,
+ url: url,
+ video_count: short_text_to_number(video_count_txt || ""),
+ channel_count: short_text_to_number(channel_count_txt || ""),
+ })
+ rescue ex
+ LOGGER.debug("HashtagRendererParser: Failed to extract renderer.")
+ LOGGER.debug("HashtagRendererParser: Got exception: #{ex.message}")
+ return nil
+ end
+
+ def self.parser_name
+ return {{@type.name}}
+ end
+ end
+
# Parses a InnerTube gridPlaylistRenderer into a SearchPlaylist. Returns nil when the given object isn't a gridPlaylistRenderer
#
# A gridPlaylistRenderer renders a playlist, that is located in a grid, to click on within the YouTube and Invidious UI.