summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/invidious.cr178
-rw-r--r--src/invidious/helpers/handlers.cr7
-rw-r--r--src/invidious/search.cr161
-rw-r--r--src/invidious/users.cr24
-rw-r--r--src/invidious/videos.cr114
-rw-r--r--src/invidious/views/watch.ecr6
6 files changed, 270 insertions, 220 deletions
diff --git a/src/invidious.cr b/src/invidious.cr
index 0203e3b8..8f7e1a63 100644
--- a/src/invidious.cr
+++ b/src/invidious.cr
@@ -148,26 +148,32 @@ statistics = {
}
if config.statistics_enabled
spawn do
- loop do
- statistics = {
- "version" => "2.0",
- "software" => SOFTWARE,
- "openRegistrations" => config.registration_enabled,
- "usage" => {
- "users" => {
- "total" => PG_DB.query_one("SELECT count(*) FROM users", as: Int64),
- "activeHalfyear" => PG_DB.query_one("SELECT count(*) FROM users WHERE CURRENT_TIMESTAMP - updated < '6 months'", as: Int64),
- "activeMonth" => PG_DB.query_one("SELECT count(*) FROM users WHERE CURRENT_TIMESTAMP - updated < '1 month'", as: Int64),
- },
- },
- "metadata" => {
- "updatedAt" => Time.utc.to_unix,
- "lastChannelRefreshedAt" => PG_DB.query_one?("SELECT updated FROM channels ORDER BY updated DESC LIMIT 1", as: Time).try &.to_unix || 0,
+ statistics = {
+ "version" => "2.0",
+ "software" => SOFTWARE,
+ "openRegistrations" => config.registration_enabled,
+ "usage" => {
+ "users" => {
+ "total" => PG_DB.query_one("SELECT count(*) FROM users", as: Int64),
+ "activeHalfyear" => PG_DB.query_one("SELECT count(*) FROM users WHERE CURRENT_TIMESTAMP - updated < '6 months'", as: Int64),
+ "activeMonth" => PG_DB.query_one("SELECT count(*) FROM users WHERE CURRENT_TIMESTAMP - updated < '1 month'", as: Int64),
},
- }
+ },
+ "metadata" => {
+ "updatedAt" => Time.utc.to_unix,
+ "lastChannelRefreshedAt" => PG_DB.query_one?("SELECT updated FROM channels ORDER BY updated DESC LIMIT 1", as: Time).try &.to_unix || 0_i64,
+ },
+ }
+ loop do
sleep 1.minute
Fiber.yield
+
+ statistics["usage"].as(Hash)["users"].as(Hash)["total"] = PG_DB.query_one("SELECT count(*) FROM users", as: Int64)
+ statistics["usage"].as(Hash)["users"].as(Hash)["activeHalfyear"] = PG_DB.query_one("SELECT count(*) FROM users WHERE CURRENT_TIMESTAMP - updated < '6 months'", as: Int64)
+ statistics["usage"].as(Hash)["users"].as(Hash)["activeMonth"] = PG_DB.query_one("SELECT count(*) FROM users WHERE CURRENT_TIMESTAMP - updated < '1 month'", as: Int64)
+ statistics["metadata"].as(Hash(String, Int64))["updatedAt"] = Time.utc.to_unix
+ statistics["metadata"].as(Hash(String, Int64))["lastChannelRefreshedAt"] = PG_DB.query_one?("SELECT updated FROM channels ORDER BY updated DESC LIMIT 1", as: Time).try &.to_unix || 0_i64
end
end
end
@@ -3223,35 +3229,35 @@ get "/api/v1/storyboards/:id" do |env|
storyboard = storyboard[0]
end
- webvtt = <<-END_VTT
- WEBVTT
+ String.build do |str|
+ str << <<-END_VTT
+ WEBVTT
- END_VTT
+ END_VTT
- start_time = 0.milliseconds
- end_time = storyboard[:interval].milliseconds
+ start_time = 0.milliseconds
+ end_time = storyboard[:interval].milliseconds
- storyboard[:storyboard_count].times do |i|
- host_url = make_host_url(config, Kemal.config)
- url = storyboard[:url].gsub("$M", i).gsub("https://i9.ytimg.com", host_url)
+ storyboard[:storyboard_count].times do |i|
+ host_url = make_host_url(config, Kemal.config)
+ url = storyboard[:url].gsub("$M", i).gsub("https://i9.ytimg.com", host_url)
- storyboard[:storyboard_height].times do |j|
- storyboard[:storyboard_width].times do |k|
- webvtt += <<-END_CUE
- #{start_time}.000 --> #{end_time}.000
- #{url}#xywh=#{storyboard[:width] * k},#{storyboard[:height] * j},#{storyboard[:width]},#{storyboard[:height]}
+ storyboard[:storyboard_height].times do |j|
+ storyboard[:storyboard_width].times do |k|
+ str << <<-END_CUE
+ #{start_time}.000 --> #{end_time}.000
+ #{url}#xywh=#{storyboard[:width] * k},#{storyboard[:height] * j},#{storyboard[:width]},#{storyboard[:height]}
- END_CUE
+ END_CUE
- start_time += storyboard[:interval].milliseconds
- end_time += storyboard[:interval].milliseconds
+ start_time += storyboard[:interval].milliseconds
+ end_time += storyboard[:interval].milliseconds
+ end
end
end
end
-
- webvtt
end
get "/api/v1/captions/:id" do |env|
@@ -3321,7 +3327,7 @@ get "/api/v1/captions/:id" do |env|
caption = caption[0]
end
- url = caption.baseUrl + "&tlang=#{tlang}"
+ url = "#{caption.baseUrl}&tlang=#{tlang}"
# Auto-generated captions often have cues that aren't aligned properly with the video,
# as well as some other markup that makes it cumbersome, so we try to fix that here
@@ -3329,46 +3335,47 @@ get "/api/v1/captions/:id" do |env|
caption_xml = client.get(url).body
caption_xml = XML.parse(caption_xml)
- webvtt = <<-END_VTT
- WEBVTT
- Kind: captions
- Language: #{tlang || caption.languageCode}
+ webvtt = String.build do |str|
+ str << <<-END_VTT
+ WEBVTT
+ Kind: captions
+ Language: #{tlang || caption.languageCode}
- END_VTT
+ END_VTT
- caption_nodes = caption_xml.xpath_nodes("//transcript/text")
- caption_nodes.each_with_index do |node, i|
- start_time = node["start"].to_f.seconds
- duration = node["dur"]?.try &.to_f.seconds
- duration ||= start_time
+ caption_nodes = caption_xml.xpath_nodes("//transcript/text")
+ caption_nodes.each_with_index do |node, i|
+ start_time = node["start"].to_f.seconds
+ duration = node["dur"]?.try &.to_f.seconds
+ duration ||= start_time
- if caption_nodes.size > i + 1
- end_time = caption_nodes[i + 1]["start"].to_f.seconds
- else
- end_time = start_time + duration
- end
+ if caption_nodes.size > i + 1
+ end_time = caption_nodes[i + 1]["start"].to_f.seconds
+ else
+ end_time = start_time + duration
+ end
- start_time = "#{start_time.hours.to_s.rjust(2, '0')}:#{start_time.minutes.to_s.rjust(2, '0')}:#{start_time.seconds.to_s.rjust(2, '0')}.#{start_time.milliseconds.to_s.rjust(3, '0')}"
- end_time = "#{end_time.hours.to_s.rjust(2, '0')}:#{end_time.minutes.to_s.rjust(2, '0')}:#{end_time.seconds.to_s.rjust(2, '0')}.#{end_time.milliseconds.to_s.rjust(3, '0')}"
+ start_time = "#{start_time.hours.to_s.rjust(2, '0')}:#{start_time.minutes.to_s.rjust(2, '0')}:#{start_time.seconds.to_s.rjust(2, '0')}.#{start_time.milliseconds.to_s.rjust(3, '0')}"
+ end_time = "#{end_time.hours.to_s.rjust(2, '0')}:#{end_time.minutes.to_s.rjust(2, '0')}:#{end_time.seconds.to_s.rjust(2, '0')}.#{end_time.milliseconds.to_s.rjust(3, '0')}"
- text = HTML.unescape(node.content)
- text = text.gsub(/<font color="#[a-fA-F0-9]{6}">/, "")
- text = text.gsub(/<\/font>/, "")
- if md = text.match(/(?<name>.*) : (?<text>.*)/)
- text = "<v #{md["name"]}>#{md["text"]}</v>"
- end
+ text = HTML.unescape(node.content)
+ text = text.gsub(/<font color="#[a-fA-F0-9]{6}">/, "")
+ text = text.gsub(/<\/font>/, "")
+ if md = text.match(/(?<name>.*) : (?<text>.*)/)
+ text = "<v #{md["name"]}>#{md["text"]}</v>"
+ end
- webvtt += <<-END_CUE
- #{start_time} --> #{end_time}
- #{text}
+ str << <<-END_CUE
+ #{start_time} --> #{end_time}
+ #{text}
- END_CUE
+ END_CUE
+ end
end
else
- url += "&format=vtt"
- webvtt = client.get(url).body
+ webvtt = client.get("#{url}&format=vtt").body
end
if title = env.params.query["title"]?
@@ -4833,12 +4840,24 @@ get "/videoplayback" do |env|
end
end
+ client = make_client(URI.parse(host), region)
+
response = HTTP::Client::Response.new(403)
5.times do
begin
- client = make_client(URI.parse(host), region)
response = client.head(url, headers)
- break
+
+ if response.headers["Location"]?
+ location = URI.parse(response.headers["Location"])
+ env.response.headers["Access-Control-Allow-Origin"] = "*"
+
+ host = "#{location.scheme}://#{location.host}"
+ client = make_client(URI.parse(host), region)
+
+ url = "#{location.full_path}&host=#{location.host}#{region ? "&region=#{region}" : ""}"
+ else
+ break
+ end
rescue Socket::Addrinfo::Error
if !mns.empty?
mn = mns.pop
@@ -4846,25 +4865,11 @@ get "/videoplayback" do |env|
fvip = "3"
host = "https://r#{fvip}---#{mn}.googlevideo.com"
+ client = make_client(URI.parse(host), region)
rescue ex
end
end
- if response.headers["Location"]?
- url = URI.parse(response.headers["Location"])
- host = url.host
- env.response.headers["Access-Control-Allow-Origin"] = "*"
-
- url = url.full_path
- url += "&host=#{host}"
-
- if region
- url += "&region=#{region}"
- end
-
- next env.redirect url
- end
-
if response.status_code >= 400
env.response.status_code = response.status_code
next
@@ -4921,6 +4926,8 @@ get "/videoplayback" do |env|
chunk_end = chunk_start + HTTP_CHUNK_SIZE - 1
end
+ client = make_client(URI.parse(host), region)
+
# TODO: Record bytes written so we can restart after a chunk fails
while true
if !range_end && content_length
@@ -4938,7 +4945,6 @@ get "/videoplayback" do |env|
headers["Range"] = "bytes=#{chunk_start}-#{chunk_end}"
begin
- client = make_client(URI.parse(host), region)
client.get(url, headers) do |response|
if first_chunk
if !env.request.headers["Range"]? && response.status_code == 206
@@ -4957,11 +4963,7 @@ get "/videoplayback" do |env|
if location = response.headers["Location"]?
location = URI.parse(location)
- location = "#{location.full_path}&host=#{location.host}"
-
- if region
- location += "&region=#{region}"
- end
+ location = "#{location.full_path}&host=#{location.host}#{region ? "&region=#{region}" : ""}"
env.redirect location
break
@@ -4988,6 +4990,8 @@ get "/videoplayback" do |env|
rescue ex
if ex.message != "Error reading socket: Connection reset by peer"
break
+ else
+ client = make_client(URI.parse(host), region)
end
end
diff --git a/src/invidious/helpers/handlers.cr b/src/invidious/helpers/handlers.cr
index 51bc9545..5965923d 100644
--- a/src/invidious/helpers/handlers.cr
+++ b/src/invidious/helpers/handlers.cr
@@ -159,10 +159,9 @@ class APIHandler < Kemal::Handler
call_next env
env.response.output.rewind
- response = env.response.output.gets_to_end
- if env.response.headers["Content-Type"]?.try &.== "application/json"
- response = JSON.parse(response)
+ if env.response.headers.includes_word?("Content-Type", "application/json")
+ response = JSON.parse(env.response.output)
if fields_text = env.params.query["fields"]?
begin
@@ -178,6 +177,8 @@ class APIHandler < Kemal::Handler
else
response = response.to_json
end
+ else
+ response = env.response.output.gets_to_end
end
rescue ex
ensure
diff --git a/src/invidious/search.cr b/src/invidious/search.cr
index 7a36f32e..a55bb216 100644
--- a/src/invidious/search.cr
+++ b/src/invidious/search.cr
@@ -277,96 +277,97 @@ end
def produce_search_params(sort : String = "relevance", date : String = "", content_type : String = "",
duration : String = "", features : Array(String) = [] of String)
- head = "\x08"
- head += case sort
- when "relevance"
- "\x00"
- when "rating"
- "\x01"
- when "upload_date", "date"
- "\x02"
- when "view_count", "views"
- "\x03"
- else
- raise "No sort #{sort}"
- end
+ header = IO::Memory.new
+ header.write Bytes[0x08]
+ header.write case sort
+ when "relevance"
+ Bytes[0x00]
+ when "rating"
+ Bytes[0x01]
+ when "upload_date", "date"
+ Bytes[0x02]
+ when "view_count", "views"
+ Bytes[0x03]
+ else
+ raise "No sort #{sort}"
+ end
- body = ""
- body += case date
- when "hour"
- "\x08\x01"
- when "today"
- "\x08\x02"
- when "week"
- "\x08\x03"
- when "month"
- "\x08\x04"
- when "year"
- "\x08\x05"
- else
- ""
- end
+ body = IO::Memory.new
+ body.write case date
+ when "hour"
+ Bytes[0x08, 0x01]
+ when "today"
+ Bytes[0x08, 0x02]
+ when "week"
+ Bytes[0x08, 0x03]
+ when "month"
+ Bytes[0x08, 0x04]
+ when "year"
+ Bytes[0x08, 0x05]
+ else
+ Bytes.new(0)
+ end
- body += case content_type
- when "video"
- "\x10\x01"
- when "channel"
- "\x10\x02"
- when "playlist"
- "\x10\x03"
- when "movie"
- "\x10\x04"
- when "show"
- "\x10\x05"
- when "all"
- ""
- else
- "\x10\x01"
- end
+ body.write case content_type
+ when "video"
+ Bytes[0x10, 0x01]
+ when "channel"
+ Bytes[0x10, 0x02]
+ when "playlist"
+ Bytes[0x10, 0x03]
+ when "movie"
+ Bytes[0x10, 0x04]
+ when "show"
+ Bytes[0x10, 0x05]
+ when "all"
+ Bytes.new(0)
+ else
+ Bytes[0x10, 0x01]
+ end
- body += case duration
- when "short"
- "\x18\x01"
- when "long"
- "\x18\x02"
- else
- ""
- end
+ body.write case duration
+ when "short"
+ Bytes[0x18, 0x01]
+ when "long"
+ Bytes[0x18, 0x12]
+ else
+ Bytes.new(0)
+ end
features.each do |feature|
- body += case feature
- when "hd"
- "\x20\x01"
- when "subtitles"
- "\x28\x01"
- when "creative_commons", "cc"
- "\x30\x01"
- when "3d"
- "\x38\x01"
- when "live", "livestream"
- "\x40\x01"
- when "purchased"
- "\x48\x01"
- when "4k"
- "\x70\x01"
- when "360"
- "\x78\x01"
- when "location"
- "\xb8\x01\x01"
- when "hdr"
- "\xc8\x01\x01"
- else
- raise "Unknown feature #{feature}"
- end
+ body.write case feature
+ when "hd"
+ Bytes[0x20, 0x01]
+ when "subtitles"
+ Bytes[0x28, 0x01]
+ when "creative_commons", "cc"
+ Bytes[0x30, 0x01]
+ when "3d"
+ Bytes[0x38, 0x01]
+ when "live", "livestream"
+ Bytes[0x40, 0x01]
+ when "purchased"
+ Bytes[0x48, 0x01]
+ when "4k"
+ Bytes[0x70, 0x01]
+ when "360"
+ Bytes[0x78, 0x01]
+ when "location"
+ Bytes[0xb8, 0x01, 0x01]
+ when "hdr"
+ Bytes[0xc8, 0x01, 0x01]
+ else
+ Bytes.new(0)
+ end
end
+ token = header
if !body.empty?
- token = head + "\x12" + body.size.unsafe_chr + body
- else
- token = head
+ token.write Bytes[0x12, body.bytesize]
+ token.write body.to_slice
end
- token = Base64.urlsafe_encode(token)
+ token = Base64.urlsafe_encode(token.to_slice)
token = URI.escape(token)
return token
diff --git a/src/invidious/users.cr b/src/invidious/users.cr
index 8bd82bf1..d3da28d7 100644
--- a/src/invidious/users.cr
+++ b/src/invidious/users.cr
@@ -295,8 +295,7 @@ def get_subscription_feed(db, user, max_results = 40, page = 1)
args = arg_array(notifications)
- notifications = db.query_all("SELECT * FROM channel_videos WHERE id IN (#{args})
- ORDER BY published DESC", notifications, as: ChannelVideo)
+ notifications = db.query_all("SELECT * FROM channel_videos WHERE id IN (#{args}) ORDER BY published DESC", notifications, as: ChannelVideo)
videos = [] of ChannelVideo
notifications.sort_by! { |video| video.published }.reverse!
@@ -322,14 +321,11 @@ def get_subscription_feed(db, user, max_results = 40, page = 1)
else
values = "VALUES #{user.watched.map { |id| %(('#{id}')) }.join(",")}"
end
- videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM #{view_name} WHERE \
- NOT id = ANY (#{values}) \
- ORDER BY ucid, published DESC", as: ChannelVideo)
+ videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM #{view_name} WHERE NOT id = ANY (#{values}) ORDER BY ucid, published DESC", as: ChannelVideo)
else
# Show latest video from each channel
- videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM #{view_name} \
- ORDER BY ucid, published DESC", as: ChannelVideo)
+ videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM #{view_name} ORDER BY ucid, published DESC", as: ChannelVideo)
end
videos.sort_by! { |video| video.published }.reverse!
@@ -342,14 +338,11 @@ def get_subscription_feed(db, user, max_results = 40, page = 1)
else
values = "VALUES #{user.watched.map { |id| %(('#{id}')) }.join(",")}"
end
- videos = PG_DB.query_all("SELECT * FROM #{view_name} WHERE \
- NOT id = ANY (#{values}) \
- ORDER BY published DESC LIMIT $1 OFFSET $2", limit, offset, as: ChannelVideo)
+ videos = PG_DB.query_all("SELECT * FROM #{view_name} WHERE NOT id = ANY (#{values}) ORDER BY published DESC LIMIT $1 OFFSET $2", limit, offset, as: ChannelVideo)
else
# Sort subscriptions as normal
- videos = PG_DB.query_all("SELECT * FROM #{view_name} \
- ORDER BY published DESC LIMIT $1 OFFSET $2", limit, offset, as: ChannelVideo)
+ videos = PG_DB.query_all("SELECT * FROM #{view_name} ORDER BY published DESC LIMIT $1 OFFSET $2", limit, offset, as: ChannelVideo)
end
end
@@ -366,16 +359,11 @@ def get_subscription_feed(db, user, max_results = 40, page = 1)
videos.sort_by! { |video| video.author }.reverse!
end
- notifications = PG_DB.query_one("SELECT notifications FROM users WHERE email = $1", user.email,
- as: Array(String))
+ notifications = PG_DB.query_one("SELECT notifications FROM users WHERE email = $1", user.email, as: Array(String))
notifications = videos.select { |v| notifications.includes? v.id }
videos = videos - notifications
end
- if !limit
- videos = videos[0..max_results]
- end
-
return videos, notifications
end
diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr
index 03fe9a26..ef3f4d4b 100644
--- a/src/invidious/videos.cr
+++ b/src/invidious/videos.cr
@@ -247,6 +247,7 @@ end
struct Video
property player_json : JSON::Any?
+ property recommended_json : JSON::Any?
module HTTPParamConverter
def self.from_rs(rs)
@@ -425,9 +426,29 @@ struct Video
json.field "videoThumbnails" do
generate_thumbnails(json, rv["id"], config, kemal_config)
end
+
json.field "author", rv["author"]
+ json.field "authorUrl", rv["author_url"]?
+ json.field "authorId", rv["ucid"]?
+ if rv["author_thumbnail"]?
+ json.field "authorThumbnails" do
+ json.array do
+ qualities = {32, 48, 76, 100, 176, 512}
+
+ qualities.each do |quality|
+ json.object do
+ json.field "url", rv["author_thumbnail"].gsub(/s\d+-/, "s#{quality}-")
+ json.field "width", quality
+ json.field "height", quality
+ end
+ end
+ end
+ end
+ end
+
json.field "lengthSeconds", rv["length_seconds"].to_i
json.field "viewCountText", rv["short_view_count_text"]
+ json.field "viewCount", rv["view_count"]?.try &.to_i64
end
end
end
@@ -685,12 +706,14 @@ struct Video
return audio_streams
end
- def player_response
- if !@player_json
- @player_json = JSON.parse(@info["player_response"])
- end
+ def recommended_videos
+ @recommended_json = JSON.parse(@info["recommended_videos"]) if !@recommended_json
+ @recommended_json.not_nil!
+ end
- return @player_json.not_nil!
+ def player_response
+ @player_json = JSON.parse(@info["player_response"]) if !@player_json
+ @player_json.not_nil!
end
def storyboards
@@ -903,6 +926,33 @@ def get_video(id, db, refresh = true, region = nil, force_refresh = false)
return video
end
+def extract_recommended(recommended_videos)
+ rvs = [] of HTTP::Params
+
+ recommended_videos.try &.each do |compact_renderer|
+ if compact_renderer["compactRadioRenderer"]? || compact_renderer["compactPlaylistRenderer"]?
+ # TODO
+ elsif video_renderer = compact_renderer["compactVideoRenderer"]?
+ recommended_video = HTTP::Params.new
+ recommended_video["id"] = video_renderer["videoId"].as_s
+ recommended_video["title"] = video_renderer["title"]["simpleText"].as_s
+ recommended_video["author"] = video_renderer["shortBylineText"]["runs"].as_a[0]["text"].as_s
+ recommended_video["ucid"] = video_renderer["shortBylineText"]["runs"].as_a[0]["navigationEndpoint"]["browseEndpoint"]["browseId"].as_s
+ recommended_video["author_thumbnail"] = video_renderer["channelThumbnail"]["thumbnails"][0]["url"].as_s
+
+ if view_count = video_renderer["viewCountText"]?.try { |field| field["simpleText"]?.try &.as_s || field["runs"][0]?.try &.["text"].as_s }.try &.delete(", views watching").to_i64?.try &.to_s
+ recommended_video["view_count"] = view_count
+ recommended_video["short_view_count_text"] = "#{number_to_short_text(view_count.to_i64)} views"
+ end
+ recommended_video["length_seconds"] = decode_length_seconds(video_renderer["lengthText"]?.try &.["simpleText"]?.try &.as_s || "0:00").to_s
+
+ rvs << recommended_video
+ end
+ end
+
+ rvs
+end
+
def extract_polymer_config(body, html)
params = HTTP::Params.new
@@ -933,36 +983,14 @@ def extract_polymer_config(body, html)
params["ctoken"] = comment_continuation.try &.["continuation"]?.try &.as_s || ""
params["itct"] = comment_continuation.try &.["clickTrackingParams"]?.try &.as_s || ""
- recommended_videos = initial_data["contents"]?
+ rvs = initial_data["contents"]?
.try &.["twoColumnWatchNextResults"]?
.try &.["secondaryResults"]?
.try &.["secondaryResults"]?
.try &.["results"]?
.try &.as_a
- rvs = [] of String
-
- recommended_videos.try &.each do |compact_renderer|
- if compact_renderer["compactRadioRenderer"]? || compact_renderer["compactPlaylistRenderer"]?
- # TODO
- elsif compact_renderer["compactVideoRenderer"]?
- compact_renderer = compact_renderer["compactVideoRenderer"]
-
- recommended_video = HTTP::Params.new
- recommended_video["id"] = compact_renderer["videoId"].as_s
- recommended_video["title"] = compact_renderer["title"]["simpleText"].as_s
- recommended_video["author"] = compact_renderer["shortBylineText"]["runs"].as_a[0]["text"].as_s
- recommended_video["ucid"] = compact_renderer["shortBylineText"]["runs"].as_a[0]["navigationEndpoint"]["browseEndpoint"]["browseId"].as_s
- recommended_video["author_thumbnail"] = compact_renderer["channelThumbnail"]["thumbnails"][0]["url"].as_s
-
- recommended_video["short_view_count_text"] = compact_renderer["shortViewCountText"]["simpleText"].as_s
- recommended_video["view_count"] = compact_renderer["viewCountText"]?.try &.["simpleText"]?.try &.as_s.delete(", views watching").to_i64?.try &.to_s || "0"
- recommended_video["length_seconds"] = decode_length_seconds(compact_renderer["lengthText"]?.try &.["simpleText"]?.try &.as_s || "0:00").to_s
-
- rvs << recommended_video.to_s
- end
- end
- params["rvs"] = rvs.join(",")
+ params["rvs"] = extract_recommended(rvs).join(",")
# TODO: Watching now
params["views"] = primary_results.try &.as_a.select { |object| object["videoPrimaryInfoRenderer"]? }[0]?
@@ -1072,8 +1100,32 @@ def extract_player_config(body, html)
params["session_token"] = md["session_token"]
end
- if md = body.match(/'RELATED_PLAYER_ARGS': (?<rvs>{"rvs":"[^"]+"})/)
- params["rvs"] = JSON.parse(md["rvs"])["rvs"].as_s
+ if md = body.match(/'RELATED_PLAYER_ARGS': (?<json>.*?),\n/)
+ recommended_json = JSON.parse(md["json"])
+ rvs_params = recommended_json["rvs"].as_s.split(",").map { |params| HTTP::Params.parse(params) }
+
+ if watch_next_response = recommended_json["watch_next_response"]?
+ watch_next_json = JSON.parse(watch_next_response.as_s)
+ rvs = watch_next_json["contents"]?
+ .try &.["twoColumnWatchNextResults"]?
+ .try &.["secondaryResults"]?
+ .try &.["secondaryResults"]?
+ .try &.["results"]?
+ .try &.as_a
+
+ rvs = extract_recommended(rvs)
+ rvs.each_with_index do |rv, i|
+ if !rv["view_count"]?
+ rv_params = rvs_params.select { |rv_params| rv_params["id"]? == (rv["id"]? || "") }[0]?
+ if rv_params
+ rvs[i]["short_view_count_text"] = rv_params["short_view_count_text"]
+ else
+ rvs.delete_at(i)
+ end
+ end
+ end
+ params["rvs"] = (rvs.map &.to_s).join(",")
+ end
end
html_info = body.match(/ytplayer\.config = (?<info>.*?);ytplayer\.load/).try &.["info"]
diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr
index 542fe9e5..58c4b1a3 100644
--- a/src/invidious/views/watch.ecr
+++ b/src/invidious/views/watch.ecr
@@ -235,7 +235,11 @@ var video_data = {
<p style="width:100%"><%= rv["title"] %></p>
<h5 class="pure-g">
<div class="pure-u-14-24">
- <b style="width:100%"><%= rv["author"] %></b>
+ <% if rv["ucid"]? %>
+ <b style="width:100%"><a href="/channel/<%= rv["ucid"] %>"><%= rv["author"] %></a></b>
+ <% else %>
+ <b style="width:100%"><%= rv["author"] %></b>
+ <% end %>
</div>
<div class="pure-u-10-24" style="text-align:right">