([a-zA-Z0-9_-]{11})|(\w+))$/)
item = md["id"]
diff --git a/src/invidious/user/imports.cr b/src/invidious/user/imports.cr
index 0a2fe1e2..86d0ce6e 100644
--- a/src/invidious/user/imports.cr
+++ b/src/invidious/user/imports.cr
@@ -133,7 +133,7 @@ struct Invidious::User
next if !video_id
begin
- video = get_video(video_id)
+ video = get_video(video_id, false)
rescue ex
next
end
diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr
index 2a09d187..06ff96b1 100644
--- a/src/invidious/videos/parser.cr
+++ b/src/invidious/videos/parser.cr
@@ -55,9 +55,7 @@ def extract_video_info(video_id : String, proxy_region : String? = nil)
client_config = YoutubeAPI::ClientConfig.new(proxy_region: proxy_region)
# Fetch data from the player endpoint
- # CgIQBg is a workaround for streaming URLs that returns a 403.
- # See https://github.com/iv-org/invidious/issues/4027#issuecomment-1666944520
- player_response = YoutubeAPI.player(video_id: video_id, params: "CgIQBg", client_config: client_config)
+ player_response = YoutubeAPI.player(video_id: video_id, params: "", client_config: client_config)
playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s
@@ -120,6 +118,9 @@ def extract_video_info(video_id : String, proxy_region : String? = nil)
# Replace player response and reset reason
if !new_player_response.nil?
+ # Preserve storyboard data before replacement
+ new_player_response["storyboards"] = player_response["storyboards"] if player_response["storyboards"]?
+
player_response = new_player_response
params.delete("reason")
end
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 @@
<%= 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"
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.