From c8ecfaabe156e41999cf3a130a28a67a62b37ccb Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 16 Jul 2023 17:28:37 +0200 Subject: [PATCH 1/4] Assets: Add SVG image for hashtag results --- assets/hashtag.svg | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 assets/hashtag.svg diff --git a/assets/hashtag.svg b/assets/hashtag.svg new file mode 100644 index 000000000..55109825f --- /dev/null +++ b/assets/hashtag.svg @@ -0,0 +1,9 @@ + + + + + + + + + From 839e90aeff93a18d59cb4fc53eb25cc5c152b44a Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 15 Jul 2023 15:41:04 +0200 Subject: [PATCH 2/4] Extractors: Add module for 'hashtagTileRenderer' --- src/invidious/helpers/serialized_yt_data.cr | 21 +++++++- src/invidious/yt_backend/extractors.cr | 53 ++++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/invidious/helpers/serialized_yt_data.cr b/src/invidious/helpers/serialized_yt_data.cr index 7c12ad0e3..e0bd7279b 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/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index e5029dc5c..8456313be 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. From f38d1f33b140a1de13e20d14b7a1ff0fcf0a40b4 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 15 Jul 2023 15:42:46 +0200 Subject: [PATCH 3/4] HTML: Add UI element for 'SearchHashtag' in item.ecr --- src/invidious/views/components/item.ecr | 26 ++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index 7ffd2d937..c29ec47b6 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 @@

<%= translate_count(locale, "generic_subscribers_count", item.subscriber_count, NumberFormatting::Separator) %>

<% if !item.auto_generated %>

<%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %>

<% end %>
<%= item.description_html %>
+ <% when SearchHashtag %> + <% if !thin_mode %> + +
+
+ <%- else -%> +
+ <% end %> + + + +
+ <%- if item.video_count != 0 -%> +

<%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %>

+ <%- end -%> +
+ +
+ <%- if item.channel_count != 0 -%> +

<%= translate_count(locale, "generic_channels_count", item.channel_count, NumberFormatting::Separator) %>

+ <%- end -%> +
<% when SearchPlaylist, InvidiousPlaylist %> <%- if item.id.starts_with? "RD" From 70b80ce8ad5ad9e5eb57a8f2f8e72a2274f8523f Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 28 Jul 2023 08:11:15 +0200 Subject: [PATCH 4/4] I18n: Add translation strings for new feature (fr/en) --- locales/en-US.json | 2 ++ locales/fr.json | 2 ++ 2 files changed, 4 insertions(+) diff --git a/locales/en-US.json b/locales/en-US.json index 74f43d905..06d095dcf 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -1,4 +1,6 @@ { + "generic_channels_count": "{{count}} channel", + "generic_channels_count_plural": "{{count}} channels", "generic_views_count": "{{count}} view", "generic_views_count_plural": "{{count}} views", "generic_videos_count": "{{count}} video", diff --git a/locales/fr.json b/locales/fr.json index 2eb4dd2b0..c48c8be53 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1,4 +1,6 @@ { + "generic_channels_count": "{{count}} chaîne", + "generic_channels_count_plural": "{{count}} chaînes", "generic_views_count": "{{count}} vue", "generic_views_count_plural": "{{count}} vues", "generic_videos_count": "{{count}} vidéo",