-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add invidious companion support #4985
base: master
Are you sure you want to change the base?
Changes from 20 commits
3dff7a7
73c84ba
1954463
c612423
2cc204a
1c9f5b0
27b24f5
409df4c
ff3305d
1aa154b
9f84612
b51770d
bb2e3b2
734e725
1f51edd
7a070fa
f710dd3
a571eea
ab72bba
1de2054
0dba767
e9c354d
f550359
bfaf72b
84b87be
a5acdde
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -201,6 +201,13 @@ module Invidious::Routes::Embed | |
return env.redirect url | ||
end | ||
|
||
if companion_base_url = video.invidious_companion.try &.["baseUrl"].as_s | ||
env.response.headers["Content-Security-Policy"] = | ||
env.response.headers["Content-Security-Policy"] | ||
.gsub("media-src", "media-src #{companion_base_url}") | ||
.gsub("connect-src", "connect-src #{companion_base_url}") | ||
end | ||
unixfox marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+204
to
+209
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you move these to the before_all handler instead maybe under a: if {"/embed", "/watch"}.any? { |r| env.request.resource.starts_with? r }
env.response.headers["Content-Security-Policy"] =
env.response.headers["Content-Security-Policy"]
.gsub("media-src", "media-src #{companion_base_url}")
.gsub("connect-src", "connect-src #{companion_base_url}")
end |
||
|
||
rendered "embed" | ||
end | ||
end |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -100,30 +100,32 @@ def extract_video_info(video_id : String) | |||||
params = parse_video_info(video_id, player_response) | ||||||
params["reason"] = JSON::Any.new(reason) if reason | ||||||
|
||||||
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 | ||||||
if !CONFIG.invidious_companion.empty? | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
The Invidious stream data workarounds should run when invidious companion is not set |
||||||
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"]? | ||||||
# 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") | ||||||
player_response = new_player_response | ||||||
params.delete("reason") | ||||||
end | ||||||
end | ||||||
|
||||||
{"captions", "playabilityStatus", "playerConfig", "storyboards"}.each do |f| | ||||||
{"captions", "playabilityStatus", "playerConfig", "storyboards", "invidiousCompanion"}.each do |f| | ||||||
params[f] = player_response[f] if player_response[f]? | ||||||
end | ||||||
|
||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -500,7 +500,11 @@ module YoutubeAPI | |
data["params"] = params | ||
end | ||
|
||
return self._post_json("/youtubei/v1/player", data, client_config) | ||
if !CONFIG.invidious_companion.empty? | ||
return self._post_invidious_companion("/youtubei/v1/player", data) | ||
unixfox marked this conversation as resolved.
Show resolved
Hide resolved
|
||
else | ||
return self._post_json("/youtubei/v1/player", data, client_config) | ||
end | ||
end | ||
|
||
#################################################################### | ||
|
@@ -666,6 +670,51 @@ module YoutubeAPI | |
return initial_data | ||
end | ||
|
||
#################################################################### | ||
# _post_invidious_companion(endpoint, data) | ||
# | ||
# Internal function that does the actual request to Invidious companion | ||
# and handles errors. | ||
# | ||
# The requested data is an endpoint (URL without the domain part) | ||
# and the data as a Hash object. | ||
# | ||
def _post_invidious_companion( | ||
endpoint : String, | ||
data : Hash | ||
) : Hash(String, JSON::Any) | ||
headers = HTTP::Headers{ | ||
"Content-Type" => "application/json; charset=UTF-8", | ||
"Authorization" => "Bearer #{CONFIG.invidious_companion_key}", | ||
} | ||
|
||
# Logging | ||
LOGGER.debug("Invidious companion: Using endpoint: \"#{endpoint}\"") | ||
LOGGER.trace("Invidious companion: POST data: #{data}") | ||
|
||
# Send the POST request | ||
|
||
begin | ||
invidious_companion = CONFIG.invidious_companion.sample | ||
response = make_client(invidious_companion.private_url, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since Invidious is expected to constantly make requests to the Invidious companion shouldn't it use a connection pool? It shouldn't be too difficult to modify the connection pool's factory method to randomly select a companion URL for each client it creates. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you suggest how to do that? I'm not very familiar with this stuff. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I haven't tested it but something like this should do the trick: Patchdiff --git a/src/invidious.cr b/src/invidious.cr
index b422dcbb..c0c78a79 100644
--- a/src/invidious.cr
+++ b/src/invidious.cr
@@ -97,6 +97,10 @@ YT_POOL = YoutubeConnectionPool.new(YT_URL, capacity: CONFIG.pool_size)
GGPHT_POOL = YoutubeConnectionPool.new(URI.parse("https://yt3.ggpht.com"), capacity: CONFIG.pool_size)
+COMPANION_POOL = CompanionConnectionPool.new(
+ capacity: CONFIG.pool_size
+)
+
# CLI
Kemal.config.extra_options do |parser|
parser.banner = "Usage: invidious [arguments]"
diff --git a/src/invidious/yt_backend/connection_pool.cr b/src/invidious/yt_backend/connection_pool.cr
index c4a73aa7..6f1ef9bd 100644
--- a/src/invidious/yt_backend/connection_pool.cr
+++ b/src/invidious/yt_backend/connection_pool.cr
@@ -46,6 +46,45 @@ struct YoutubeConnectionPool
end
end
+struct CompanionConnectionPool
+ property pool : DB::Pool(HTTP::Client)
+
+ def initialize(capacity = 5, timeout = 5.0)
+ options = DB::Pool::Options.new(
+ initial_pool_size: 0,
+ max_pool_size: capacity,
+ max_idle_pool_size: capacity,
+ checkout_timeout: timeout
+ )
+
+ @pool = DB::Pool(HTTP::Client).new(options) do
+ companion = CONFIG.invidious_companion.sample
+ next make_client(companion.private_url, force_resolve: true)
+ end
+ end
+
+ def client(&)
+ conn = pool.checkout
+ # Proxy needs to be reinstated every time we get a client from the pool
+ conn.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy
+
+ begin
+ response = yield conn
+ rescue ex
+ conn.close
+
+ companion = CONFIG.invidious_companion.sample
+ conn = make_client(companion.private_url, force_resolve: true)
+
+ response = yield conn
+ ensure
+ pool.release(conn)
+ end
+
+ response
+ end
+end
+
def add_yt_headers(request)
request.headers.delete("User-Agent") if request.headers["User-Agent"] == "Crystal"
request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr
index 74f65449..9bc6fe05 100644
--- a/src/invidious/yt_backend/youtube_api.cr
+++ b/src/invidious/yt_backend/youtube_api.cr
@@ -695,9 +695,7 @@ module YoutubeAPI
# Send the POST request
begin
- invidious_companion = CONFIG.invidious_companion.sample
- response = make_client(invidious_companion.private_url,
- &.post(endpoint, headers: headers, body: data.to_json))
+ response = COMPANION_POOL.client &.post(endpoint, headers: headers, body: data.to_json)
body = response.body
if (response.status_code != 200)
raise Exception.new( Keep in mind that the connection pool will have the behavior described here #4326 (comment)
unixfox marked this conversation as resolved.
Show resolved
Hide resolved
|
||
&.post(endpoint, headers: headers, body: data.to_json)) | ||
body = response.body | ||
if (response.status_code != 200) | ||
raise Exception.new( | ||
"Error while communicating with Invidious companion: \ | ||
status code: #{response.status_code} and body: #{body.dump}" | ||
) | ||
end | ||
rescue ex | ||
raise InfoException.new("Error while communicating with Invidious companion: " + (ex.message || "no extra info found")) | ||
end | ||
|
||
# Convert result to Hash | ||
initial_data = JSON.parse(body).as_h | ||
|
||
return initial_data | ||
end | ||
|
||
#################################################################### | ||
# _decompress(body_io, headers) | ||
# | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Codestlye nitpick
What do you think about replacing
!empty?
withpresent?
instead orunless {...}.empty?
I've seen a couple discussions in the Crystal community regarding
if !empty?
being harder to process cognitively due to an essentially double negation.Related discussions:
https://forum.crystal-lang.org/t/collections-any-vs-empty/5303
crystal-lang/crystal#13847
crystal-lang/shards#577 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's perfect for me. That was already odd for me to do
!empty?
, happy there is an alternative.