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.cr (renamed from src/invidious/helpers/static_file_handler.cr)0
-rw-r--r--src/invidious.cr6
-rw-r--r--src/invidious/channels/about.cr7
-rw-r--r--src/invidious/comments.cr71
-rw-r--r--src/invidious/frontend/search_filters.cr2
-rw-r--r--src/invidious/helpers/errors.cr2
-rw-r--r--src/invidious/helpers/i18n.cr2
-rw-r--r--src/invidious/helpers/macros.cr12
-rw-r--r--src/invidious/helpers/serialized_yt_data.cr7
-rw-r--r--src/invidious/helpers/utils.cr8
-rw-r--r--src/invidious/routes/api/manifest.cr10
-rw-r--r--src/invidious/routes/feeds.cr1
-rw-r--r--src/invidious/routes/watch.cr17
-rw-r--r--src/invidious/search/query.cr17
-rw-r--r--src/invidious/videos.cr54
-rw-r--r--src/invidious/views/channel.ecr2
-rw-r--r--src/invidious/views/community.ecr2
-rw-r--r--src/invidious/views/components/item.ecr18
-rw-r--r--src/invidious/views/components/player.ecr18
-rw-r--r--src/invidious/views/embed.ecr3
-rw-r--r--src/invidious/views/playlists.ecr2
-rw-r--r--src/invidious/views/watch.ecr9
-rw-r--r--src/invidious/yt_backend/extractors.cr50
24 files changed, 243 insertions, 93 deletions
diff --git a/src/ext/kemal_content_for.cr b/src/ext/kemal_content_for.cr
new file mode 100644
index 00000000..a4f3fd96
--- /dev/null
+++ b/src/ext/kemal_content_for.cr
@@ -0,0 +1,16 @@
+# 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/invidious/helpers/static_file_handler.cr b/src/ext/kemal_static_file_handler.cr
index 6ef2d74c..6ef2d74c 100644
--- a/src/invidious/helpers/static_file_handler.cr
+++ b/src/ext/kemal_static_file_handler.cr
diff --git a/src/invidious.cr b/src/invidious.cr
index 9f3d5d10..dd240852 100644
--- a/src/invidious.cr
+++ b/src/invidious.cr
@@ -16,7 +16,13 @@
require "digest/md5"
require "file_utils"
+
+# Require kemal, kilt, then our own overrides
require "kemal"
+require "kilt"
+require "./ext/kemal_content_for.cr"
+require "./ext/kemal_static_file_handler.cr"
+
require "athena-negotiation"
require "openssl/hmac"
require "option_parser"
diff --git a/src/invidious/channels/about.cr b/src/invidious/channels/about.cr
index 4f82a0f1..d48fd1fb 100644
--- a/src/invidious/channels/about.cr
+++ b/src/invidious/channels/about.cr
@@ -12,7 +12,8 @@ record AboutChannel,
joined : Time,
is_family_friendly : Bool,
allowed_regions : Array(String),
- tabs : Array(String)
+ tabs : Array(String),
+ verified : Bool
record AboutRelatedChannel,
ucid : String,
@@ -70,6 +71,9 @@ 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 = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]?.try &.as_s? || ""
description_html = HTML.escape(description)
@@ -128,6 +132,7 @@ def get_about_info(ucid, locale) : AboutChannel
is_family_friendly: is_family_friendly,
allowed_regions: allowed_regions,
tabs: tabs,
+ verified: author_verified || false,
)
end
diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr
index ab9fcc8b..1f8de657 100644
--- a/src/invidious/comments.cr
+++ b/src/invidious/comments.cr
@@ -143,9 +143,11 @@ def fetch_youtube_comments(id, cursor, format, locale, thin_mode, region, sort_b
node_comment = node["commentRenderer"]
end
- content_html = node_comment["contentText"]?.try { |t| parse_content(t) } || ""
+ content_html = node_comment["contentText"]?.try { |t| parse_content(t, id) } || ""
author = node_comment["authorText"]?.try &.["simpleText"]? || ""
+ json.field "verified", (node_comment["authorCommentBadge"]? != nil)
+
json.field "author", author
json.field "authorThumbnails" do
json.array do
@@ -329,7 +331,11 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false)
end
author_name = HTML.escape(child["author"].as_s)
-
+ if child["verified"]?.try &.as_bool && child["authorIsChannelOwner"]?.try &.as_bool
+ author_name += "&nbsp;<i class=\"icon ion ion-md-checkmark-circle\"></i>"
+ elsif child["verified"]?.try &.as_bool
+ author_name += "&nbsp;<i class=\"icon ion ion-md-checkmark\"></i>"
+ end
html << <<-END_HTML
<div class="pure-g" style="width:100%">
<div class="channel-profile pure-u-4-24 pure-u-md-2-24">
@@ -554,26 +560,19 @@ def fill_links(html, scheme, host)
return html.to_xml(options: XML::SaveOptions::NO_DECL)
end
-def parse_content(content : JSON::Any) : String
+def parse_content(content : JSON::Any, video_id : String? = "") : String
content["simpleText"]?.try &.as_s.rchop('\ufeff').try { |b| HTML.escape(b) }.to_s ||
- content["runs"]?.try &.as_a.try { |r| content_to_comment_html(r).try &.to_s.gsub("\n", "<br>") } || ""
+ content["runs"]?.try &.as_a.try { |r| content_to_comment_html(r, video_id).try &.to_s.gsub("\n", "<br>") } || ""
end
-def content_to_comment_html(content)
- comment_html = content.map do |run|
+def content_to_comment_html(content, video_id : String? = "")
+ html_array = content.map do |run|
text = HTML.escape(run["text"].as_s)
- if run["bold"]?
- text = "<b>#{text}</b>"
- end
-
- if run["italics"]?
- text = "<i>#{text}</i>"
- end
-
if run["navigationEndpoint"]?
if url = run["navigationEndpoint"]["urlEndpoint"]?.try &.["url"].as_s
url = URI.parse(url)
+ displayed_url = text
if url.host == "youtu.be"
url = "/watch?v=#{url.request_target.lstrip('/')}"
@@ -581,31 +580,53 @@ def content_to_comment_html(content)
if url.path == "/redirect"
# Sometimes, links can be corrupted (why?) so make sure to fallback
# nicely. See https://github.com/iv-org/invidious/issues/2682
- url = HTTP::Params.parse(url.query.not_nil!)["q"]? || ""
+ url = url.query_params["q"]? || ""
+ displayed_url = url
else
url = url.request_target
+ displayed_url = "youtube.com#{url}"
end
end
- text = %(<a href="#{url}">#{text}</a>)
+ text = %(<a href="#{url}">#{reduce_uri(displayed_url)}</a>)
elsif watch_endpoint = run["navigationEndpoint"]["watchEndpoint"]?
- length_seconds = watch_endpoint["startTimeSeconds"]?
- video_id = watch_endpoint["videoId"].as_s
-
- if length_seconds && length_seconds.as_i > 0
- text = %(<a href="javascript:void(0)" data-onclick="jump_to_time" data-jump-time="#{length_seconds}">#{text}</a>)
+ start_time = watch_endpoint["startTimeSeconds"]?.try &.as_i
+ link_video_id = watch_endpoint["videoId"].as_s
+
+ url = "/watch?v=#{link_video_id}"
+ url += "&t=#{start_time}" if !start_time.nil?
+
+ # If the current video ID (passed through from the caller function)
+ # is the same as the video ID in the link, add HTML attributes for
+ # the JS handler function that bypasses page reload.
+ #
+ # See: https://github.com/iv-org/invidious/issues/3063
+ if link_video_id == video_id
+ start_time ||= 0
+ text = %(<a href="#{url}" data-onclick="jump_to_time" data-jump-time="#{start_time}">#{reduce_uri(text)}</a>)
else
- text = %(<a href="/watch?v=#{video_id}">#{text}</a>)
+ text = %(<a href="#{url}">#{text}</a>)
end
elsif url = run.dig?("navigationEndpoint", "commandMetadata", "webCommandMetadata", "url").try &.as_s
- text = %(<a href="#{url}">#{text}</a>)
+ if text.starts_with?(/\s?[@#]/)
+ # Handle "pings" in comments and hasthags differently
+ # See:
+ # - https://github.com/iv-org/invidious/issues/3038
+ # - https://github.com/iv-org/invidious/issues/3062
+ text = %(<a href="#{url}">#{text}</a>)
+ else
+ text = %(<a href="#{url}">#{reduce_uri(url)}</a>)
+ end
end
end
+ text = "<b>#{text}</b>" if run["bold"]?
+ text = "<i>#{text}</i>" if run["italics"]?
+
text
- end.join("").delete('\ufeff')
+ end
- return comment_html
+ return html_array.join("").delete('\ufeff')
end
def produce_comment_continuation(video_id, cursor = "", sort_by = "top")
diff --git a/src/invidious/frontend/search_filters.cr b/src/invidious/frontend/search_filters.cr
index 68f27b4f..8ac0af2e 100644
--- a/src/invidious/frontend/search_filters.cr
+++ b/src/invidious/frontend/search_filters.cr
@@ -106,7 +106,7 @@ module Invidious::Frontend::SearchFilters
{% feature = value.underscore %}
str << "\t\t\t\t\t\t<div>"
- str << "<input type='checkbox' name='features' id='filter-features-{{feature}}' value='{{feature}}'"
+ str << "<input type='checkbox' name='features' id='filter-feature-{{feature}}' value='{{feature}}'"
str << " checked" if value.{{feature}}?
str << '>'
diff --git a/src/invidious/helpers/errors.cr b/src/invidious/helpers/errors.cr
index 2eab6263..b80dcdaf 100644
--- a/src/invidious/helpers/errors.cr
+++ b/src/invidious/helpers/errors.cr
@@ -46,7 +46,7 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce
TEXT
- issue_template += github_details("Backtrace", HTML.escape(exception.inspect_with_backtrace))
+ issue_template += github_details("Backtrace", exception.inspect_with_backtrace)
# URLs for the error message below
url_faq = "https://github.com/iv-org/documentation/blob/master/docs/faq.md"
diff --git a/src/invidious/helpers/i18n.cr b/src/invidious/helpers/i18n.cr
index 982b97d8..9d3c4e8b 100644
--- a/src/invidious/helpers/i18n.cr
+++ b/src/invidious/helpers/i18n.cr
@@ -14,6 +14,7 @@ LOCALES_LIST = {
"fi" => "Suomi", # Finnish
"fr" => "Français", # French
"he" => "עברית", # Hebrew
+ "hi" => "हिन्दी", # Hindi
"hr" => "Hrvatski", # Croatian
"hu-HU" => "Magyar Nyelv", # Hungarian
"id" => "Bahasa Indonesia", # Indonesian
@@ -30,6 +31,7 @@ LOCALES_LIST = {
"pt-PT" => "Português de Portugal", # Portuguese (Portugal)
"ro" => "Română", # Romanian
"ru" => "Русский", # Russian
+ "sl" => "Slovenščina", # Slovenian
"sq" => "Shqip", # Albanian
"sr" => "Srpski (latinica)", # Serbian (Latin)
"sr_Cyrl" => "Српски (ћирилица)", # Serbian (Cyrillic)
diff --git a/src/invidious/helpers/macros.cr b/src/invidious/helpers/macros.cr
index 75df1612..43e7171b 100644
--- a/src/invidious/helpers/macros.cr
+++ b/src/invidious/helpers/macros.cr
@@ -48,13 +48,19 @@ module JSON::Serializable
end
end
-macro templated(filename, template = "template", navbar_search = true)
+macro templated(_filename, template = "template", navbar_search = true)
navbar_search = {{navbar_search}}
- render "src/invidious/views/#{{{filename}}}.ecr", "src/invidious/views/#{{{template}}}.ecr"
+
+ {{ filename = "src/invidious/views/" + _filename + ".ecr" }}
+ {{ layout = "src/invidious/views/" + template + ".ecr" }}
+
+ __content_filename__ = {{filename}}
+ content = Kilt.render({{filename}})
+ Kilt.render({{layout}})
end
macro rendered(filename)
- render "src/invidious/views/#{{{filename}}}.ecr"
+ Kilt.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 bfbc237c..3918bd13 100644
--- a/src/invidious/helpers/serialized_yt_data.cr
+++ b/src/invidious/helpers/serialized_yt_data.cr
@@ -12,6 +12,7 @@ struct SearchVideo
property live_now : Bool
property premium : Bool
property premiere_timestamp : Time?
+ property author_verified : Bool
def to_xml(auto_generated, query_params, xml : XML::Builder)
query_params["v"] = self.id
@@ -129,6 +130,7 @@ struct SearchPlaylist
property video_count : Int32
property videos : Array(SearchPlaylistVideo)
property thumbnail : String?
+ property author_verified : Bool
def to_json(locale : String?, json : JSON::Builder)
json.object do
@@ -141,6 +143,8 @@ struct SearchPlaylist
json.field "authorId", self.ucid
json.field "authorUrl", "/channel/#{self.ucid}"
+ json.field "authorVerified", self.author_verified
+
json.field "videoCount", self.video_count
json.field "videos" do
json.array do
@@ -182,6 +186,7 @@ struct SearchChannel
property video_count : Int32
property description_html : String
property auto_generated : Bool
+ property author_verified : Bool
def to_json(locale : String?, json : JSON::Builder)
json.object do
@@ -189,7 +194,7 @@ struct SearchChannel
json.field "author", self.author
json.field "authorId", self.ucid
json.field "authorUrl", "/channel/#{self.ucid}"
-
+ json.field "authorVerified", self.author_verified
json.field "authorThumbnails" do
json.array do
qualities = {32, 48, 76, 100, 176, 512}
diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr
index c1dc17db..8ae5034a 100644
--- a/src/invidious/helpers/utils.cr
+++ b/src/invidious/helpers/utils.cr
@@ -383,3 +383,11 @@ def fetch_random_instance
return filtered_instance_list.sample(1)[0]
end
+
+def reduce_uri(uri : URI | String, max_length : Int32 = 50, suffix : String = "…") : String
+ str = uri.to_s.sub(/^https?:\/\//, "")
+ if str.size > max_length
+ str = "#{str[0, max_length]}#{suffix}"
+ end
+ return str
+end
diff --git a/src/invidious/routes/api/manifest.cr b/src/invidious/routes/api/manifest.cr
index ca429df5..8bc36946 100644
--- a/src/invidious/routes/api/manifest.cr
+++ b/src/invidious/routes/api/manifest.cr
@@ -56,12 +56,15 @@ module Invidious::Routes::API::Manifest
xml.element("Period") do
i = 0
- {"audio/mp4", "audio/webm"}.each do |mime_type|
+ {"audio/mp4"}.each do |mime_type|
mime_streams = audio_streams.select { |stream| stream["mimeType"].as_s.starts_with? mime_type }
next if mime_streams.empty?
xml.element("AdaptationSet", id: i, mimeType: mime_type, startWithSAP: 1, subsegmentAlignment: true) do
mime_streams.each do |fmt|
+ # OTF streams aren't supported yet (See https://github.com/TeamNewPipe/NewPipe/issues/2415)
+ next if !(fmt.has_key?("indexRange") && fmt.has_key?("initRange"))
+
codecs = fmt["mimeType"].as_s.split("codecs=")[1].strip('"')
bandwidth = fmt["bitrate"].as_i
itag = fmt["itag"].as_i
@@ -83,13 +86,16 @@ module Invidious::Routes::API::Manifest
potential_heights = {4320, 2160, 1440, 1080, 720, 480, 360, 240, 144}
- {"video/mp4", "video/webm"}.each do |mime_type|
+ {"video/mp4"}.each do |mime_type|
mime_streams = video_streams.select { |stream| stream["mimeType"].as_s.starts_with? mime_type }
next if mime_streams.empty?
heights = [] of Int32
xml.element("AdaptationSet", id: i, mimeType: mime_type, startWithSAP: 1, subsegmentAlignment: true, scanType: "progressive") do
mime_streams.each do |fmt|
+ # OTF streams aren't supported yet (See https://github.com/TeamNewPipe/NewPipe/issues/2415)
+ next if !(fmt.has_key?("indexRange") && fmt.has_key?("initRange"))
+
codecs = fmt["mimeType"].as_s.split("codecs=")[1].strip('"')
bandwidth = fmt["bitrate"].as_i
itag = fmt["itag"].as_i
diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr
index f7f7b426..b5b58399 100644
--- a/src/invidious/routes/feeds.cr
+++ b/src/invidious/routes/feeds.cr
@@ -182,6 +182,7 @@ module Invidious::Routes::Feeds
paid: false,
premium: false,
premiere_timestamp: nil,
+ author_verified: false, # ¯\_(ツ)_/¯
})
end
diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr
index 867ffa6a..75475430 100644
--- a/src/invidious/routes/watch.cr
+++ b/src/invidious/routes/watch.cr
@@ -308,25 +308,26 @@ module Invidious::Routes::Watch
extension = download_widget["ext"].as_s
filename = "#{video_id}-#{title}.#{extension}"
- # Pass form parameters as URL parameters for the handlers of both
- # /latest_version and /api/v1/captions. This avoids an un-necessary
- # redirect and duplicated (and hazardous) sanity checks.
- env.params.query["id"] = video_id
- env.params.query["title"] = filename
-
- # Delete the useless ones
+ # Delete the now useless URL parameters
env.params.body.delete("id")
env.params.body.delete("title")
env.params.body.delete("download_widget")
+ # Pass form parameters as URL parameters for the handlers of both
+ # /latest_version and /api/v1/captions. This avoids an un-necessary
+ # redirect and duplicated (and hazardous) sanity checks.
if label = download_widget["label"]?
# URL params specific to /api/v1/captions/:id
- env.params.query["label"] = URI.encode_www_form(label.as_s, space_to_plus: false)
+ env.params.url["id"] = video_id
+ env.params.query["title"] = filename
+ env.params.query["label"] = URI.decode_www_form(label.as_s)
return Invidious::Routes::API::V1::Videos.captions(env)
elsif itag = download_widget["itag"]?.try &.as_i
# URL params specific to /latest_version
+ env.params.query["id"] = video_id
env.params.query["itag"] = itag.to_s
+ env.params.query["title"] = filename
env.params.query["local"] = "true"
return Invidious::Routes::VideoPlayback.latest_version(env)
diff --git a/src/invidious/search/query.cr b/src/invidious/search/query.cr
index 1c2b37d2..34b36b1d 100644
--- a/src/invidious/search/query.cr
+++ b/src/invidious/search/query.cr
@@ -10,7 +10,7 @@ module Invidious::Search
Playlist # "Add playlist item" search
end
- @type : Type = Type::Regular
+ getter type : Type = Type::Regular
@raw_query : String
@query : String = ""
@@ -63,14 +63,17 @@ module Invidious::Search
# Specific handling
case @type
- when .playlist?, .channel?
- # In "add playlist item" mode, filters are parsed from the query
- # string itself (legacy), and the channel is ignored.
- #
+ when .channel?
# In "channel search" mode, filters are ignored, but we still parse
# the query prevent transmission of legacy filters to youtube.
#
- @filters, @query, @channel, _ = Filters.from_legacy_filters(@raw_query || "")
+ _, _, @query, _ = Filters.from_legacy_filters(@raw_query)
+ #
+ when .playlist?
+ # In "add playlist item" mode, filters are parsed from the query
+ # string itself (legacy), and the channel is ignored.
+ #
+ @filters, _, @query, _ = Filters.from_legacy_filters(@raw_query)
#
when .subscriptions?, .regular?
if params["sp"]?
@@ -84,7 +87,7 @@ module Invidious::Search
if @filters.default? && @raw_query.includes?(':')
# Parse legacy filters from query
- @filters, @query, @channel, subs = Filters.from_legacy_filters(@raw_query || "")
+ @filters, @channel, @query, subs = Filters.from_legacy_filters(@raw_query)
else
@query = @raw_query || ""
end
diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr
index 31ae90c7..f65b05bb 100644
--- a/src/invidious/videos.cr
+++ b/src/invidious/videos.cr
@@ -374,18 +374,25 @@ struct Video
json.array do
self.adaptive_fmts.each do |fmt|
json.object do
- json.field "index", "#{fmt["indexRange"]["start"]}-#{fmt["indexRange"]["end"]}"
- json.field "bitrate", fmt["bitrate"].as_i.to_s
- json.field "init", "#{fmt["initRange"]["start"]}-#{fmt["initRange"]["end"]}"
+ # Only available on regular videos, not livestreams/OTF streams
+ if init_range = fmt["initRange"]?
+ json.field "init", "#{init_range["start"]}-#{init_range["end"]}"
+ end
+ if index_range = fmt["indexRange"]?
+ json.field "index", "#{index_range["start"]}-#{index_range["end"]}"
+ end
+
+ # Not available on MPEG-4 Timed Text (`text/mp4`) streams (livestreams only)
+ json.field "bitrate", fmt["bitrate"].as_i.to_s if fmt["bitrate"]?
+
json.field "url", fmt["url"]
json.field "itag", fmt["itag"].as_i.to_s
json.field "type", fmt["mimeType"]
- json.field "clen", fmt["contentLength"]
+ json.field "clen", fmt["contentLength"]? || "-1"
json.field "lmt", fmt["lastModified"]
json.field "projectionType", fmt["projectionType"]
- fmt_info = itag_to_metadata?(fmt["itag"])
- if fmt_info
+ if fmt_info = itag_to_metadata?(fmt["itag"])
fps = fmt_info["fps"]?.try &.to_i || fmt["fps"]?.try &.as_i || 30
json.field "fps", fps
json.field "container", fmt_info["ext"]
@@ -405,6 +412,19 @@ struct Video
end
end
end
+
+ # Livestream chunk infos
+ json.field "targetDurationSec", fmt["targetDurationSec"].as_i if fmt.has_key?("targetDurationSec")
+ json.field "maxDvrDurationSec", fmt["maxDvrDurationSec"].as_i if fmt.has_key?("maxDvrDurationSec")
+
+ # Audio-related data
+ json.field "audioQuality", fmt["audioQuality"] if fmt.has_key?("audioQuality")
+ json.field "audioSampleRate", fmt["audioSampleRate"].as_s.to_i if fmt.has_key?("audioSampleRate")
+ json.field "audioChannels", fmt["audioChannels"] if fmt.has_key?("audioChannels")
+
+ # Extra misc stuff
+ json.field "colorInfo", fmt["colorInfo"] if fmt.has_key?("colorInfo")
+ json.field "captionTrack", fmt["captionTrack"] if fmt.has_key?("captionTrack")
end
end
end
@@ -593,6 +613,10 @@ struct Video
info["authorThumbnail"]?.try &.as_s || ""
end
+ def author_verified : Bool
+ info["authorVerified"]?.try &.as_bool || false
+ end
+
def sub_count_text : String
info["subCountText"]?.try &.as_s || "-"
end
@@ -612,6 +636,7 @@ struct Video
fmt["url"] = JSON::Any.new("#{fmt["url"]}&host=#{URI.parse(fmt["url"].as_s).host}")
fmt["url"] = JSON::Any.new("#{fmt["url"]}&region=#{self.info["region"]}") if self.info["region"]?
end
+
fmt_stream.sort_by! { |f| f["width"]?.try &.as_i || 0 }
@fmt_stream = fmt_stream
return @fmt_stream.as(Array(Hash(String, JSON::Any)))
@@ -631,9 +656,7 @@ struct Video
fmt["url"] = JSON::Any.new("#{fmt["url"]}&host=#{URI.parse(fmt["url"].as_s).host}")
fmt["url"] = JSON::Any.new("#{fmt["url"]}&region=#{self.info["region"]}") if self.info["region"]?
end
- # See https://github.com/TeamNewPipe/NewPipe/issues/2415
- # Some streams are segmented by URL `sq/` rather than index, for now we just filter them out
- fmt_stream.reject! { |f| !f["indexRange"]? }
+
fmt_stream.sort_by! { |f| f["width"]?.try &.as_i || 0 }
@adaptive_fmts = fmt_stream
return @adaptive_fmts.as(Array(Hash(String, JSON::Any)))
@@ -845,6 +868,12 @@ 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
+
ucid = channel_info.try { |ci| HelperExtractors.get_browse_id(ci) }
# "4,088,033 views", only available on compact renderer
@@ -868,6 +897,7 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
"length_seconds" => JSON::Any.new(length || "0"),
"view_count" => JSON::Any.new(view_count || "0"),
"short_view_count" => JSON::Any.new(short_view_count || "0"),
+ "author_verified" => JSON::Any.new(author_verified),
}
end
@@ -1024,7 +1054,7 @@ def extract_video_info(video_id : String, proxy_region : String? = nil, context_
# Description
description_html = video_secondary_renderer.try &.dig?("description", "runs")
- .try &.as_a.try { |t| content_to_comment_html(t) }
+ .try &.as_a.try { |t| content_to_comment_html(t, video_id) }
params["descriptionHtml"] = JSON::Any.new(description_html || "<p></p>")
@@ -1062,6 +1092,10 @@ def extract_video_info(video_id : String, proxy_region : String? = nil, context_
author_info = video_secondary_renderer.try &.dig?("owner", "videoOwnerRenderer")
author_thumbnail = author_info.try &.dig?("thumbnail", "thumbnails", 0, "url")
+ 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)
+
params["authorThumbnail"] = JSON::Any.new(author_thumbnail.try &.as_s || "")
params["subCountText"] = JSON::Any.new(author_info.try &.["subscriberCountText"]?
diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr
index 40b553a9..92f81ee4 100644
--- a/src/invidious/views/channel.ecr
+++ b/src/invidious/views/channel.ecr
@@ -20,7 +20,7 @@
<div class="pure-u-2-3">
<div class="channel-profile">
<img src="/ggpht<%= URI.parse(channel.author_thumbnail).request_target %>">
- <span><%= author %></span>
+ <span><%= author %></span><% if !channel.verified.nil? && channel.verified %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %>
</div>
</div>
<div class="pure-u-1-3">
diff --git a/src/invidious/views/community.ecr b/src/invidious/views/community.ecr
index f0add06b..3bc29e55 100644
--- a/src/invidious/views/community.ecr
+++ b/src/invidious/views/community.ecr
@@ -19,7 +19,7 @@
<div class="pure-u-2-3">
<div class="channel-profile">
<img src="/ggpht<%= URI.parse(channel.author_thumbnail).request_target %>">
- <span><%= author %></span>
+ <span><%= author %></span><% if !channel.verified.nil? && channel.verified %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %>
</div>
</div>
<div class="pure-u-1-3" style="text-align:right">
diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr
index 5f8bde13..fb7ad1dc 100644
--- a/src/invidious/views/components/item.ecr
+++ b/src/invidious/views/components/item.ecr
@@ -8,7 +8,7 @@
<img loading="lazy" style="width:56.25%" src="/ggpht<%= URI.parse(item.author_thumbnail).request_target.gsub(/=s\d+/, "=s176") %>"/>
</center>
<% end %>
- <p dir="auto"><%= HTML.escape(item.author) %></p>
+ <p dir="auto"><%= HTML.escape(item.author) %><% if !item.author_verified.nil? && item.author_verified %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %></p>
</a>
<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 %>
@@ -30,7 +30,7 @@
<p dir="auto"><%= HTML.escape(item.title) %></p>
</a>
<a href="/channel/<%= item.ucid %>">
- <p dir="auto"><b><%= HTML.escape(item.author) %></b></p>
+ <p dir="auto"><b><%= HTML.escape(item.author) %><% if !item.is_a?(InvidiousPlaylist) && !item.author_verified.nil? && item.author_verified %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %></b></p>
</a>
<% when MixVideo %>
<a href="/watch?v=<%= item.id %>&list=<%= item.rdid %>">
@@ -52,11 +52,11 @@
<% if !env.get("preferences").as(Preferences).thin_mode %>
<div class="thumbnail">
<img loading="lazy" class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
- <% if plid = env.get?("remove_playlist_items") %>
- <form data-onsubmit="return_false" action="/playlist_ajax?action_remove_video=1&set_video_id=<%= item.index %>&playlist_id=<%= plid %>&referer=<%= env.get("current_page") %>" method="post">
+ <% if plid_form = env.get?("remove_playlist_items") %>
+ <form data-onsubmit="return_false" action="/playlist_ajax?action_remove_video=1&set_video_id=<%= item.index %>&playlist_id=<%= plid_form %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<p class="watched">
- <a data-onclick="remove_playlist_item" data-index="<%= item.index %>" data-plid="<%= plid %>" href="javascript:void(0)">
+ <a data-onclick="remove_playlist_item" data-index="<%= item.index %>" data-plid="<%= plid_form %>" href="javascript:void(0)">
<button type="submit" style="all:unset">
<i class="icon ion-md-trash"></i>
</button>
@@ -117,11 +117,11 @@
</a>
</p>
</form>
- <% elsif plid = env.get? "add_playlist_items" %>
- <form data-onsubmit="return_false" action="/playlist_ajax?action_add_video=1&video_id=<%= item.id %>&playlist_id=<%= plid %>&referer=<%= env.get("current_page") %>" method="post">
+ <% elsif plid_form = env.get? "add_playlist_items" %>
+ <form data-onsubmit="return_false" action="/playlist_ajax?action_add_video=1&video_id=<%= item.id %>&playlist_id=<%= plid_form %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<p class="watched">
- <a data-onclick="add_playlist_item" data-id="<%= item.id %>" data-plid="<%= plid %>" href="javascript:void(0)">
+ <a data-onclick="add_playlist_item" data-id="<%= item.id %>" data-plid="<%= plid_form %>" href="javascript:void(0)">
<button type="submit" style="all:unset">
<i class="icon ion-md-add"></i>
</button>
@@ -142,7 +142,7 @@
<div class="video-card-row flexible">
<div class="flex-left"><a href="/channel/<%= item.ucid %>">
- <p class="channel-name" dir="auto"><%= HTML.escape(item.author) %></p>
+ <p class="channel-name" dir="auto"><%= HTML.escape(item.author) %><% if !item.is_a?(ChannelVideo) && !item.author_verified.nil? && item.author_verified %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %></p>
</a></div>
<% endpoint_params = "?v=#{item.id}" %>
diff --git a/src/invidious/views/components/player.ecr b/src/invidious/views/components/player.ecr
index 206ba380..fffefc9a 100644
--- a/src/invidious/views/components/player.ecr
+++ b/src/invidious/views/components/player.ecr
@@ -7,8 +7,19 @@
<source src="<%= URI.parse(hlsvp).request_target %><% if params.local %>?local=true<% end %>" type="application/x-mpegURL" label="livestream">
<% else %>
<% if params.listen %>
- <% audio_streams.each_with_index do |fmt, i| %>
- <source src="/latest_version?id=<%= video.id %>&itag=<%= fmt["itag"] %><% if params.local %>&local=true<% end %>" type='<%= fmt["mimeType"] %>' label="<%= fmt["bitrate"] %>k" selected="<%= i == 0 ? true : false %>">
+ <% audio_streams.each_with_index do |fmt, i|
+ src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
+ src_url += "&local=true" if params.local
+
+ bitrate = fmt["bitrate"]
+ mimetype = HTML.escape(fmt["mimeType"].as_s)
+
+ selected = i == 0 ? true : false
+ %>
+ <source src="<%= src_url %>" type='<%= mimetype %>' label="<%= bitrate %>k" selected="<%= selected %>">
+ <% if !params.local && !CONFIG.disabled?("local") %>
+ <source src="<%= src_url %>&local=true" type='<%= mimetype %>' hidequalityoption="true">
+ <% end %>
<% end %>
<% else %>
<% if params.quality == "dash" %>
@@ -28,6 +39,9 @@
selected = params.quality ? (params.quality == quality) : (i == 0)
%>
<source src="<%= src_url %>" type="<%= mimetype %>" label="<%= quality %>" selected="<%= selected %>">
+ <% if !params.local && !CONFIG.disabled?("local") %>
+ <source src="<%= src_url %>&local=true" type="<%= mimetype %>" hidequalityoption="true">
+ <% end %>
<% end %>
<% end %>
diff --git a/src/invidious/views/embed.ecr b/src/invidious/views/embed.ecr
index 27a8e266..ce5ff7f0 100644
--- a/src/invidious/views/embed.ecr
+++ b/src/invidious/views/embed.ecr
@@ -24,7 +24,8 @@
"video_series" => video_series,
"params" => params,
"preferences" => preferences,
- "premiere_timestamp" => video.premiere_timestamp.try &.to_unix
+ "premiere_timestamp" => video.premiere_timestamp.try &.to_unix,
+ "local_disabled" => CONFIG.disabled?("local")
}.to_pretty_json
%>
</script>
diff --git a/src/invidious/views/playlists.ecr b/src/invidious/views/playlists.ecr
index 12dba088..c8718e7b 100644
--- a/src/invidious/views/playlists.ecr
+++ b/src/invidious/views/playlists.ecr
@@ -19,7 +19,7 @@
<div class="pure-u-2-3">
<div class="channel-profile">
<img src="/ggpht<%= URI.parse(channel.author_thumbnail).request_target %>">
- <span><%= author %></span>
+ <span><%= author %></span><% if !channel.verified.nil? && channel.verified %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %>
</div>
</div>
<div class="pure-u-1-3" style="text-align:right">
diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr
index 0e4af3ab..8b6eb903 100644
--- a/src/invidious/views/watch.ecr
+++ b/src/invidious/views/watch.ecr
@@ -64,7 +64,8 @@ we're going to need to do it here in order to allow for translations.
"preferences" => preferences,
"premiere_timestamp" => video.premiere_timestamp.try &.to_unix,
"vr" => video.is_vr,
- "projection_type" => video.projection_type
+ "projection_type" => video.projection_type,
+ "local_disabled" => CONFIG.disabled?("local")
}.to_pretty_json
%>
</script>
@@ -206,7 +207,7 @@ we're going to need to do it here in order to allow for translations.
<% if !video.author_thumbnail.empty? %>
<img src="/ggpht<%= URI.parse(video.author_thumbnail).request_target %>">
<% end %>
- <span id="channel-name"><%= author %></span>
+ <span id="channel-name"><%= author %><% if !video.author_verified.nil? && video.author_verified %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %></span>
</div>
</a>
@@ -280,9 +281,9 @@ we're going to need to do it here in order to allow for translations.
<h5 class="pure-g">
<div class="pure-u-14-24">
<% if rv["ucid"]? %>
- <b style="width:100%"><a href="/channel/<%= rv["ucid"] %>"><%= rv["author"]? %></a></b>
+ <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"]? %></b>
+ <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>
diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr
index ce39bc28..a2ec7d59 100644
--- a/src/invidious/yt_backend/extractors.cr
+++ b/src/invidious/yt_backend/extractors.cr
@@ -69,7 +69,7 @@ private module Parsers
# TODO change default value to nil and typical encoding type to tuple storing type (watchers, views, etc)
# and count
view_count = item_contents.dig?("viewCountText", "simpleText").try &.as_s.gsub(/\D+/, "").to_i64? || 0_i64
- description_html = item_contents["descriptionSnippet"]?.try { |t| parse_content(t) } || ""
+ description_html = item_contents["descriptionSnippet"]?.try { |t| parse_content(t, video_id) } || ""
# The length information generally exist in "lengthText". However, the info can sometimes
# be retrieved from "thumbnailOverlays" (e.g when the video is a "shorts" one).
@@ -102,7 +102,11 @@ 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
@@ -129,6 +133,7 @@ private module Parsers
live_now: live_now,
premium: premium,
premiere_timestamp: premiere_timestamp,
+ author_verified: author_verified || false,
})
end
@@ -156,7 +161,11 @@ 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_thumbnail = HelperExtractors.get_thumbnails(item_contents)
# When public subscriber count is disabled, the subscriberCountText isn't sent by InnerTube.
# Always simpleText
@@ -179,6 +188,7 @@ private module Parsers
video_count: video_count,
description_html: description_html,
auto_generated: auto_generated,
+ author_verified: author_verified || false,
})
end
@@ -206,18 +216,23 @@ 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)
video_count = HelperExtractors.get_video_count(item_contents)
playlist_thumbnail = HelperExtractors.get_thumbnails(item_contents)
SearchPlaylist.new({
- title: title,
- id: plid,
- author: author_fallback.name,
- ucid: author_fallback.id,
- video_count: video_count,
- videos: [] of SearchPlaylistVideo,
- thumbnail: playlist_thumbnail,
+ title: title,
+ id: plid,
+ author: author_fallback.name,
+ ucid: author_fallback.id,
+ video_count: video_count,
+ videos: [] of SearchPlaylistVideo,
+ thumbnail: playlist_thumbnail,
+ author_verified: author_verified || false,
})
end
@@ -251,7 +266,11 @@ 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 = (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 || ""
@@ -267,13 +286,14 @@ private module Parsers
# TODO: item_contents["publishedTimeText"]?
SearchPlaylist.new({
- title: title,
- id: plid,
- author: author,
- ucid: author_id,
- video_count: video_count,
- videos: videos,
- thumbnail: playlist_thumbnail,
+ title: title,
+ id: plid,
+ author: author,
+ ucid: author_id,
+ video_count: video_count,
+ videos: videos,
+ thumbnail: playlist_thumbnail,
+ author_verified: author_verified || false,
})
end