summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/ext/kemal_content_for.cr16
-rw-r--r--src/ext/kemal_static_file_handler.cr2
-rw-r--r--src/invidious.cr11
-rw-r--r--src/invidious/frontend/watch_page.cr8
-rw-r--r--src/invidious/helpers/errors.cr31
-rw-r--r--src/invidious/helpers/macros.cr5
-rw-r--r--src/invidious/helpers/serialized_yt_data.cr51
-rw-r--r--src/invidious/playlists.cr6
-rw-r--r--src/invidious/routes/embed.cr8
-rw-r--r--src/invidious/routes/feeds.cr8
-rw-r--r--src/invidious/routes/video_playback.cr5
-rw-r--r--src/invidious/routes/watch.cr11
-rw-r--r--src/invidious/trending.cr4
-rw-r--r--src/invidious/videos/parser.cr35
-rw-r--r--src/invidious/views/components/item.ecr14
-rw-r--r--src/invidious/yt_backend/extractors.cr95
16 files changed, 207 insertions, 103 deletions
diff --git a/src/ext/kemal_content_for.cr b/src/ext/kemal_content_for.cr
deleted file mode 100644
index a4f3fd96..00000000
--- a/src/ext/kemal_content_for.cr
+++ /dev/null
@@ -1,16 +0,0 @@
-# Overrides for Kemal's `content_for` macro in order to keep using
-# kilt as it was before Kemal v1.1.1 (Kemal PR #618).
-
-require "kemal"
-require "kilt"
-
-macro content_for(key, file = __FILE__)
- %proc = ->() {
- __kilt_io__ = IO::Memory.new
- {{ yield }}
- __kilt_io__.to_s
- }
-
- CONTENT_FOR_BLOCKS[{{key}}] = Tuple.new {{file}}, %proc
- nil
-end
diff --git a/src/ext/kemal_static_file_handler.cr b/src/ext/kemal_static_file_handler.cr
index eb068aeb..a5f42261 100644
--- a/src/ext/kemal_static_file_handler.cr
+++ b/src/ext/kemal_static_file_handler.cr
@@ -71,7 +71,7 @@ def send_file(env : HTTP::Server::Context, file_path : String, data : Slice(UInt
filesize = data.bytesize
attachment(env, filename, disposition)
- Kemal.config.static_headers.try(&.call(env.response, file_path, filestat))
+ Kemal.config.static_headers.try(&.call(env, file_path, filestat))
file = IO::Memory.new(data)
if env.request.method == "GET" && env.request.headers.has_key?("Range")
diff --git a/src/invidious.cr b/src/invidious.cr
index d3300ece..69f8a26c 100644
--- a/src/invidious.cr
+++ b/src/invidious.cr
@@ -17,10 +17,8 @@
require "digest/md5"
require "file_utils"
-# Require kemal, kilt, then our own overrides
+# Require kemal, then our own overrides
require "kemal"
-require "kilt"
-require "./ext/kemal_content_for.cr"
require "./ext/kemal_static_file_handler.cr"
require "http_proxy"
@@ -49,7 +47,8 @@ require "./invidious/channels/*"
require "./invidious/user/*"
require "./invidious/search/*"
require "./invidious/routes/**"
-require "./invidious/jobs/**"
+require "./invidious/jobs/base_job"
+require "./invidious/jobs/*"
# Declare the base namespace for invidious
module Invidious
@@ -226,8 +225,8 @@ error 500 do |env, ex|
error_template(500, ex)
end
-static_headers do |response|
- response.headers.add("Cache-Control", "max-age=2629800")
+static_headers do |env|
+ env.response.headers.add("Cache-Control", "max-age=2629800")
end
# Init Kemal
diff --git a/src/invidious/frontend/watch_page.cr b/src/invidious/frontend/watch_page.cr
index 2e2f6ad0..15d925e3 100644
--- a/src/invidious/frontend/watch_page.cr
+++ b/src/invidious/frontend/watch_page.cr
@@ -23,10 +23,16 @@ module Invidious::Frontend::WatchPage
return "<p id=\"download\">#{translate(locale, "Download is disabled")}</p>"
end
+ url = "/download"
+ if (CONFIG.invidious_companion.present?)
+ invidious_companion = CONFIG.invidious_companion.sample
+ url = "#{invidious_companion.public_url}/download?check=#{invidious_companion_encrypt(video.id)}"
+ end
+
return String.build(4000) do |str|
str << "<form"
str << " class=\"pure-form pure-form-stacked\""
- str << " action='/download'"
+ str << " action='#{url}'"
str << " method='post'"
str << " rel='noopener'"
str << " target='_blank'>"
diff --git a/src/invidious/helpers/errors.cr b/src/invidious/helpers/errors.cr
index 900cb0c6..e2c4b650 100644
--- a/src/invidious/helpers/errors.cr
+++ b/src/invidious/helpers/errors.cr
@@ -18,16 +18,7 @@ def github_details(summary : String, content : String)
return HTML.escape(details)
end
-def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exception : Exception)
- if exception.is_a?(InfoException)
- return error_template_helper(env, status_code, exception.message || "")
- end
-
- locale = env.get("preferences").as(Preferences).locale
-
- env.response.content_type = "text/html"
- env.response.status_code = status_code
-
+def get_issue_template(env : HTTP::Server::Context, exception : Exception) : Tuple(String, String)
issue_title = "#{exception.message} (#{exception.class})"
issue_template = <<-TEXT
@@ -40,6 +31,24 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce
issue_template += github_details("Backtrace", exception.inspect_with_backtrace)
+ return issue_title, issue_template
+end
+
+def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exception : Exception)
+ if exception.is_a?(InfoException)
+ return error_template_helper(env, status_code, exception.message || "")
+ end
+
+ locale = env.get("preferences").as(Preferences).locale
+
+ env.response.content_type = "text/html"
+ env.response.status_code = status_code
+
+ # Unpacking into issue_title, issue_template directly causes a compiler error
+ # I have no idea why.
+ issue_template_components = get_issue_template(env, exception)
+ issue_title, issue_template = issue_template_components
+
# URLs for the error message below
url_faq = "https://github.com/iv-org/documentation/blob/master/docs/faq.md"
url_search_issues = "https://github.com/iv-org/invidious/issues"
@@ -69,7 +78,7 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce
<p>#{translate(locale, "crash_page_report_issue", url_new_issue)}</p>
<!-- TODO: Add a "copy to clipboard" button -->
- <pre style="padding: 20px; background: rgba(0, 0, 0, 0.12345);">#{issue_template}</pre>
+ <pre class="error-issue-template">#{issue_template}</pre>
</div>
END_HTML
diff --git a/src/invidious/helpers/macros.cr b/src/invidious/helpers/macros.cr
index 43e7171b..84847321 100644
--- a/src/invidious/helpers/macros.cr
+++ b/src/invidious/helpers/macros.cr
@@ -55,12 +55,11 @@ macro templated(_filename, template = "template", navbar_search = true)
{{ layout = "src/invidious/views/" + template + ".ecr" }}
__content_filename__ = {{filename}}
- content = Kilt.render({{filename}})
- Kilt.render({{layout}})
+ render {{filename}}, {{layout}}
end
macro rendered(filename)
- Kilt.render("src/invidious/views/#{{{filename}}}.ecr")
+ render("src/invidious/views/#{{{filename}}}.ecr")
end
# Similar to Kemals halt method but works in a
diff --git a/src/invidious/helpers/serialized_yt_data.cr b/src/invidious/helpers/serialized_yt_data.cr
index f8e8f187..2796a8dc 100644
--- a/src/invidious/helpers/serialized_yt_data.cr
+++ b/src/invidious/helpers/serialized_yt_data.cr
@@ -291,6 +291,55 @@ struct SearchHashtag
end
end
+# A `ProblematicTimelineItem` is a `SearchItem` created by Invidious that
+# represents an item that caused an exception during parsing.
+#
+# This is not a parsed object from YouTube but rather an Invidious-only type
+# created to gracefully communicate parse errors without throwing away
+# the rest of the (hopefully) successfully parsed item on a page.
+struct ProblematicTimelineItem
+ property parse_exception : Exception
+ property id : String
+
+ def initialize(@parse_exception)
+ @id = Random.new.hex(8)
+ end
+
+ def to_json(locale : String?, json : JSON::Builder)
+ json.object do
+ json.field "type", "parse-error"
+ json.field "errorMessage", @parse_exception.message
+ json.field "errorBacktrace", @parse_exception.inspect_with_backtrace
+ end
+ end
+
+ # Provides compatibility with PlaylistVideo
+ def to_json(json : JSON::Builder, *args, **kwargs)
+ return to_json("", json)
+ end
+
+ def to_xml(env, locale, xml : XML::Builder)
+ xml.element("entry") do
+ xml.element("id") { xml.text "iv-err-#{@id}" }
+ xml.element("title") { xml.text "Parse Error: This item has failed to parse" }
+ xml.element("updated") { xml.text Time.utc.to_rfc3339 }
+
+ xml.element("content", type: "xhtml") do
+ xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
+ xml.element("div") do
+ xml.element("h4") { translate(locale, "timeline_parse_error_placeholder_heading") }
+ xml.element("p") { translate(locale, "timeline_parse_error_placeholder_message") }
+ end
+
+ xml.element("pre") do
+ get_issue_template(env, @parse_exception)
+ end
+ end
+ end
+ end
+ end
+end
+
class Category
include DB::Serializable
@@ -333,4 +382,4 @@ struct Continuation
end
end
-alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist | SearchHashtag | Category
+alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist | SearchHashtag | Category | ProblematicTimelineItem
diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr
index b670c009..7c584d15 100644
--- a/src/invidious/playlists.cr
+++ b/src/invidious/playlists.cr
@@ -432,7 +432,7 @@ def get_playlist_videos(playlist : InvidiousPlaylist | Playlist, offset : Int32,
offset = initial_data.dig?("contents", "twoColumnWatchNextResults", "playlist", "playlist", "currentIndex").try &.as_i || offset
end
- videos = [] of PlaylistVideo
+ videos = [] of PlaylistVideo | ProblematicTimelineItem
until videos.size >= 200 || videos.size == playlist.video_count || offset >= playlist.video_count
# 100 videos per request
@@ -448,7 +448,7 @@ def get_playlist_videos(playlist : InvidiousPlaylist | Playlist, offset : Int32,
end
def extract_playlist_videos(initial_data : Hash(String, JSON::Any))
- videos = [] of PlaylistVideo
+ videos = [] of PlaylistVideo | ProblematicTimelineItem
if initial_data["contents"]?
tabs = initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]
@@ -500,6 +500,8 @@ def extract_playlist_videos(initial_data : Hash(String, JSON::Any))
index: index,
})
end
+ rescue ex
+ videos << ProblematicTimelineItem.new(parse_exception: ex)
end
return videos
diff --git a/src/invidious/routes/embed.cr b/src/invidious/routes/embed.cr
index bdbb2d89..930e4915 100644
--- a/src/invidious/routes/embed.cr
+++ b/src/invidious/routes/embed.cr
@@ -12,13 +12,15 @@ module Invidious::Routes::Embed
url = "/playlist?list=#{plid}"
raise NotFoundException.new(translate(locale, "error_video_not_in_playlist", url))
end
+
+ first_playlist_video = videos[0].as(PlaylistVideo)
rescue ex : NotFoundException
return error_template(404, ex)
rescue ex
return error_template(500, ex)
end
- url = "/embed/#{videos[0].id}?#{env.params.query}"
+ url = "/embed/#{first_playlist_video}?#{env.params.query}"
if env.params.query.size > 0
url += "?#{env.params.query}"
@@ -72,13 +74,15 @@ module Invidious::Routes::Embed
url = "/playlist?list=#{plid}"
raise NotFoundException.new(translate(locale, "error_video_not_in_playlist", url))
end
+
+ first_playlist_video = videos[0].as(PlaylistVideo)
rescue ex : NotFoundException
return error_template(404, ex)
rescue ex
return error_template(500, ex)
end
- url = "/embed/#{videos[0].id}"
+ url = "/embed/#{first_playlist_video.id}"
elsif video_series
url = "/embed/#{video_series.shift}"
env.params.query["playlist"] = video_series.join(",")
diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr
index 7f9a0edb..abfea9ee 100644
--- a/src/invidious/routes/feeds.cr
+++ b/src/invidious/routes/feeds.cr
@@ -296,7 +296,13 @@ module Invidious::Routes::Feeds
xml.element("name") { xml.text playlist.author }
end
- videos.each &.to_xml(xml)
+ videos.each do |video|
+ if video.is_a? PlaylistVideo
+ video.to_xml(xml)
+ else
+ video.to_xml(env, locale, xml)
+ end
+ end
end
end
else
diff --git a/src/invidious/routes/video_playback.cr b/src/invidious/routes/video_playback.cr
index b1c788c2..083087a9 100644
--- a/src/invidious/routes/video_playback.cr
+++ b/src/invidious/routes/video_playback.cr
@@ -21,7 +21,7 @@ module Invidious::Routes::VideoPlayback
end
# Sanity check, to avoid being used as an open proxy
- if !host.matches?(/[\w-]+.googlevideo.com/)
+ if !host.matches?(/[\w-]+\.(?:googlevideo|c\.youtube)\.com/)
return error_template(400, "Invalid \"host\" parameter.")
end
@@ -37,7 +37,8 @@ module Invidious::Routes::VideoPlayback
# See: https://github.com/iv-org/invidious/issues/3302
range_header = env.request.headers["Range"]?
- if range_header.nil?
+ sq = query_params["sq"]?
+ if range_header.nil? && sq.nil?
range_for_head = query_params["range"]? || "0-640"
headers["Range"] = "bytes=#{range_for_head}"
end
diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr
index ab588ad6..e777b3f1 100644
--- a/src/invidious/routes/watch.cr
+++ b/src/invidious/routes/watch.cr
@@ -293,6 +293,9 @@ module Invidious::Routes::Watch
if CONFIG.disabled?("downloads")
return error_template(403, "Administrator has disabled this endpoint.")
end
+ if CONFIG.invidious_companion.present?
+ return error_template(403, "Downloads should be routed through Companion when present")
+ end
title = env.params.body["title"]? || ""
video_id = env.params.body["id"]? || ""
@@ -328,13 +331,7 @@ module Invidious::Routes::Watch
env.params.query["title"] = filename
env.params.query["local"] = "true"
- if (CONFIG.invidious_companion.present?)
- video = get_video(video_id)
- invidious_companion = CONFIG.invidious_companion.sample
- return env.redirect "#{invidious_companion.public_url}/latest_version?#{env.params.query}"
- else
- return Invidious::Routes::VideoPlayback.latest_version(env)
- end
+ return Invidious::Routes::VideoPlayback.latest_version(env)
else
return error_template(400, "Invalid label or itag")
end
diff --git a/src/invidious/trending.cr b/src/invidious/trending.cr
index 107d148d..d14cde5d 100644
--- a/src/invidious/trending.cr
+++ b/src/invidious/trending.cr
@@ -31,12 +31,12 @@ def fetch_trending(trending_type, region, locale)
# See: https://github.com/iv-org/invidious/issues/2989
next if (itm.contents.size < 24 && deduplicate)
- extracted.concat extract_category(itm)
+ extracted.concat itm.contents.select(SearchItem)
else
extracted << itm
end
end
# Deduplicate items before returning results
- return extracted.select(SearchVideo).uniq!(&.id), plid
+ return extracted.select(SearchVideo | ProblematicTimelineItem).uniq!(&.id), plid
end
diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr
index 26d74f37..15bd00b6 100644
--- a/src/invidious/videos/parser.cr
+++ b/src/invidious/videos/parser.cr
@@ -109,27 +109,20 @@ def extract_video_info(video_id : String)
params["reason"] = JSON::Any.new(reason) if reason
if !CONFIG.invidious_companion.present?
- new_player_response = nil
-
- # Don't use Android test suite client if po_token is passed because po_token doesn't
- # work for Android test suite client.
- if reason.nil? && CONFIG.po_token.nil?
- # Fetch the video streams using an Android client in order to get the
- # decrypted URLs and maybe fix throttling issues (#2194). See the
- # following issue for an explanation about decrypted URLs:
- # https://github.com/TeamNewPipe/NewPipeExtractor/issues/562
- client_config.client_type = YoutubeAPI::ClientType::AndroidTestSuite
- new_player_response = try_fetch_streaming_data(video_id, client_config)
- end
-
- # Replace player response and reset reason
- if !new_player_response.nil?
- # Preserve captions & storyboard data before replacement
- new_player_response["storyboards"] = player_response["storyboards"] if player_response["storyboards"]?
- new_player_response["captions"] = player_response["captions"] if player_response["captions"]?
-
- player_response = new_player_response
- params.delete("reason")
+ if player_response["streamingData"]? && player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil?
+ LOGGER.warn("Missing URLs for adaptive formats, falling back to other YT clients.")
+ players_fallback = [YoutubeAPI::ClientType::TvHtml5, YoutubeAPI::ClientType::WebMobile]
+ players_fallback.each do |player_fallback|
+ client_config.client_type = player_fallback
+ player_fallback_response = try_fetch_streaming_data(video_id, client_config)
+ if player_fallback_response && player_fallback_response["streamingData"]? &&
+ player_fallback_response.dig?("streamingData", "adaptiveFormats", 0, "url")
+ streaming_data = player_response["streamingData"].as_h
+ streaming_data["adaptiveFormats"] = player_fallback_response["streamingData"]["adaptiveFormats"]
+ player_response["streamingData"] = JSON::Any.new(streaming_data)
+ break
+ end
+ end
end
end
diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr
index c966a926..a24423df 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 | SearchHashtag | SearchPlaylist | InvidiousPlaylist | Category) && env.get?("user").try &.as(User).watched.index(item.id) != nil
+ item_watched = !item.is_a?(SearchChannel | SearchHashtag | SearchPlaylist | InvidiousPlaylist | Category | ProblematicTimelineItem) && env.get?("user").try &.as(User).watched.index(item.id) != nil
author_verified = item.responds_to?(:author_verified) && item.author_verified
-%>
@@ -97,6 +97,18 @@
</div>
</div>
<% when Category %>
+ <% when ProblematicTimelineItem %>
+ <div class="error-card">
+ <div class="explanation">
+ <i class="icon ion-ios-alert"></i>
+ <h4><%=translate(locale, "timeline_parse_error_placeholder_heading")%></h4>
+ <p><%=translate(locale, "timeline_parse_error_placeholder_message")%></p>
+ </div>
+ <details>
+ <summary class="pure-button pure-button-secondary"><%=translate(locale, "timeline_parse_error_show_technical_details")%></summary>
+ <pre class="error-issue-template"><%=get_issue_template(env, item.parse_exception)[1]%></pre>
+ </details>
+ </div>
<% else %>
<%-
# `endpoint_params` is used for the "video-context-buttons" component
diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr
index edd7bf1b..df2de81d 100644
--- a/src/invidious/yt_backend/extractors.cr
+++ b/src/invidious/yt_backend/extractors.cr
@@ -35,6 +35,20 @@ record AuthorFallback, name : String, id : String
# data is passed to the private `#parse()` method which returns a datastruct of the given
# type. Otherwise, nil is returned.
private module Parsers
+ module BaseParser
+ def parse(*args)
+ begin
+ return parse_internal(*args)
+ rescue ex
+ LOGGER.debug("#{{{@type.name}}}: Failed to render item.")
+ LOGGER.debug("#{{{@type.name}}}: Got exception: #{ex.message}")
+ ProblematicTimelineItem.new(
+ parse_exception: ex
+ )
+ end
+ end
+ end
+
# Parses a InnerTube videoRenderer into a SearchVideo. Returns nil when the given object isn't a videoRenderer
#
# A videoRenderer renders a video to click on within the YouTube and Invidious UI. It is **not**
@@ -45,13 +59,16 @@ private module Parsers
# `videoRenderer`s can be found almost everywhere on YouTube. In categories, search results, channels, etc.
#
module VideoRendererParser
- def self.process(item : JSON::Any, author_fallback : AuthorFallback)
+ extend self
+ include BaseParser
+
+ def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = (item["videoRenderer"]? || item["gridVideoRenderer"]?)
return self.parse(item_contents, author_fallback)
end
end
- private def self.parse(item_contents, author_fallback)
+ private def parse_internal(item_contents, author_fallback)
video_id = item_contents["videoId"].as_s
title = extract_text(item_contents["title"]?) || ""
@@ -170,13 +187,16 @@ private module Parsers
# `channelRenderer`s can be found almost everywhere on YouTube. In categories, search results, channels, etc.
#
module ChannelRendererParser
- def self.process(item : JSON::Any, author_fallback : AuthorFallback)
+ extend self
+ include BaseParser
+
+ def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = (item["channelRenderer"]? || item["gridChannelRenderer"]?)
return self.parse(item_contents, author_fallback)
end
end
- private def self.parse(item_contents, author_fallback)
+ private def parse_internal(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 = has_verified_badge?(item_contents["ownerBadges"]?)
@@ -230,13 +250,16 @@ private module Parsers
# 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)
+ extend self
+ include BaseParser
+
+ def 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)
+ private def parse_internal(item_contents)
title = extract_text(item_contents["hashtag"]).not_nil! # E.g "#hi"
# E.g "/hashtag/hi"
@@ -263,10 +286,6 @@ private module Parsers
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
@@ -284,13 +303,16 @@ private module Parsers
# `gridPlaylistRenderer`s can be found on the playlist-tabs of channels and expanded categories.
#
module GridPlaylistRendererParser
- def self.process(item : JSON::Any, author_fallback : AuthorFallback)
+ extend self
+ include BaseParser
+
+ def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["gridPlaylistRenderer"]?
return self.parse(item_contents, author_fallback)
end
end
- private def self.parse(item_contents, author_fallback)
+ private def parse_internal(item_contents, author_fallback)
title = extract_text(item_contents["title"]) || ""
plid = item_contents["playlistId"]?.try &.as_s || ""
@@ -325,13 +347,16 @@ private module Parsers
# `playlistRenderer`s can be found almost everywhere on YouTube. In categories, search results, recommended, etc.
#
module PlaylistRendererParser
- def self.process(item : JSON::Any, author_fallback : AuthorFallback)
+ extend self
+ include BaseParser
+
+ def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["playlistRenderer"]?
return self.parse(item_contents, author_fallback)
end
end
- private def self.parse(item_contents, author_fallback)
+ private def parse_internal(item_contents, author_fallback)
title = extract_text(item_contents["title"]) || ""
plid = item_contents["playlistId"]?.try &.as_s || ""
@@ -385,13 +410,16 @@ private module Parsers
# `shelfRenderer`s can be found almost everywhere on YouTube. In categories, search results, channels, etc.
#
module CategoryRendererParser
- def self.process(item : JSON::Any, author_fallback : AuthorFallback)
+ extend self
+ include BaseParser
+
+ def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["shelfRenderer"]?
return self.parse(item_contents, author_fallback)
end
end
- private def self.parse(item_contents, author_fallback)
+ private def parse_internal(item_contents, author_fallback)
title = extract_text(item_contents["title"]?) || ""
url = item_contents.dig?("endpoint", "commandMetadata", "webCommandMetadata", "url")
.try &.as_s
@@ -450,13 +478,16 @@ private module Parsers
# container.It is very similar to RichItemRendererParser
#
module ItemSectionRendererParser
- def self.process(item : JSON::Any, author_fallback : AuthorFallback)
+ extend self
+ include BaseParser
+
+ def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item.dig?("itemSectionRenderer", "contents", 0)
return self.parse(item_contents, author_fallback)
end
end
- private def self.parse(item_contents, author_fallback)
+ private def parse_internal(item_contents, author_fallback)
child = VideoRendererParser.process(item_contents, author_fallback)
child ||= PlaylistRendererParser.process(item_contents, author_fallback)
@@ -476,13 +507,16 @@ private module Parsers
# itself inside a richGridRenderer container.
#
module RichItemRendererParser
- def self.process(item : JSON::Any, author_fallback : AuthorFallback)
+ extend self
+ include BaseParser
+
+ def 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)
+ private def parse_internal(item_contents, author_fallback)
child = VideoRendererParser.process(item_contents, author_fallback)
child ||= ReelItemRendererParser.process(item_contents, author_fallback)
child ||= PlaylistRendererParser.process(item_contents, author_fallback)
@@ -506,13 +540,16 @@ private module Parsers
# TODO: Confirm that hypothesis
#
module ReelItemRendererParser
- def self.process(item : JSON::Any, author_fallback : AuthorFallback)
+ extend self
+ include BaseParser
+
+ def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["reelItemRenderer"]?
return self.parse(item_contents, author_fallback)
end
end
- private def self.parse(item_contents, author_fallback)
+ private def parse_internal(item_contents, author_fallback)
video_id = item_contents["videoId"].as_s
reel_player_overlay = item_contents.dig(
@@ -600,13 +637,16 @@ private module Parsers
# a richItemRenderer or a richGridRenderer.
#
module LockupViewModelParser
- def self.process(item : JSON::Any, author_fallback : AuthorFallback)
+ extend self
+ include BaseParser
+
+ def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["lockupViewModel"]?
return self.parse(item_contents, author_fallback)
end
end
- private def self.parse(item_contents, author_fallback)
+ private def parse_internal(item_contents, author_fallback)
playlist_id = item_contents["contentId"].as_s
thumbnail_view_model = item_contents.dig(
@@ -675,13 +715,16 @@ private module Parsers
# usually (always?) encapsulated in a richItemRenderer.
#
module ShortsLockupViewModelParser
- def self.process(item : JSON::Any, author_fallback : AuthorFallback)
+ extend self
+ include BaseParser
+
+ def process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["shortsLockupViewModel"]?
return self.parse(item_contents, author_fallback)
end
end
- private def self.parse(item_contents, author_fallback)
+ private def parse_internal(item_contents, author_fallback)
# TODO: Maybe add support for "oardefault.jpg" thumbnails?
# thumbnail = item_contents.dig("thumbnail", "sources", 0, "url").as_s
# Gives: https://i.ytimg.com/vi/{video_id}/oardefault.jpg?...