From 194fb72f34536f582be5eee119a37d120dc5915a Mon Sep 17 00:00:00 2001 From: Emilien Devos <4016501+unixfox@users.noreply.github.com> Date: Sun, 20 Oct 2024 02:10:55 +0200 Subject: [PATCH] add support for invidious companion --- config/config.example.yml | 16 ++++++ src/invidious/config.cr | 3 ++ src/invidious/videos/parser.cr | 66 +++++++++++++------------ src/invidious/yt_backend/youtube_api.cr | 30 +++++++++-- 4 files changed, 79 insertions(+), 36 deletions(-) diff --git a/config/config.example.yml b/config/config.example.yml index e9eebfdec..cac226c6d 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -54,6 +54,22 @@ db: ## #signature_server: +## +## Path to the Invidious companion. +## An external program for loading the video streams from YouTube servers. +## +## When this setting is commented out, Invidious companion is not used. +## +## When this setting is configured and "external_port" is used then +## you need to configure Invidious companion routes into your reverse proxy. +## If "external_port" is not configured then Invidious will proxy the requests +## to Invidious companion. +## +## Accepted values: "http(s)://:" +## Default: +## +#invidious_companion: + ######################################### # diff --git a/src/invidious/config.cr b/src/invidious/config.cr index a097b7f1b..937b0607a 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -138,6 +138,9 @@ class Config # poToken for passing bot attestation property po_token : String? = nil + # Invidious companion + property invidious_companion : String? = nil + # Saved cookies in "name1=value1; name2=value2..." format @[YAML::Field(converter: Preferences::StringToCookies)] property cookies : HTTP::Cookies = HTTP::Cookies.new diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index fb8935d98..1b4c16206 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -104,42 +104,44 @@ 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 - - # Second try in case WEB_CREATOR doesn't work with po_token. - # Only trigger if reason found and po_token configured. - if reason && CONFIG.po_token - client_config.client_type = YoutubeAPI::ClientType::WebEmbeddedPlayer - new_player_response = try_fetch_streaming_data(video_id, client_config) - end + if CONFIG.invidious_companion.nil? + new_player_response = nil + + # Second try in case WEB_CREATOR doesn't work with po_token. + # Only trigger if reason found and po_token configured. + if reason && CONFIG.po_token + client_config.client_type = YoutubeAPI::ClientType::WebEmbeddedPlayer + new_player_response = try_fetch_streaming_data(video_id, client_config) + end - # Don't use Android client if po_token is passed because po_token doesn't - # work for Android 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 + # Don't use Android client if po_token is passed because po_token doesn't + # work for Android 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 - # Last hope - # Only trigger if reason found or didn't work wth Android client. - # TvHtml5ScreenEmbed now requires sig helper for it to work but doesn't work with po_token. - if reason && CONFIG.po_token.nil? - client_config.client_type = YoutubeAPI::ClientType::TvHtml5ScreenEmbed - new_player_response = try_fetch_streaming_data(video_id, client_config) - end + # Last hope + # Only trigger if reason found or didn't work wth Android client. + # TvHtml5ScreenEmbed now requires sig helper for it to work but doesn't work with po_token. + if reason && CONFIG.po_token.nil? + client_config.client_type = YoutubeAPI::ClientType::TvHtml5ScreenEmbed + 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| diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index baa3cd927..212263dac 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -616,12 +616,19 @@ module YoutubeAPI headers = HTTP::Headers{ "Content-Type" => "application/json; charset=UTF-8", - "Accept-Encoding" => "gzip, deflate", "x-goog-api-format-version" => "2", "x-youtube-client-name" => client_config.name_proto, "x-youtube-client-version" => client_config.version, } + if CONFIG.invidious_companion && endpoint == "/youtubei/v1/player" + headers["Authorization"] = "Bearer " + CONFIG.hmac_key + end + + if !CONFIG.invidious_companion + headers["Accept-Encoding"] = "gzip, deflate" + end + if user_agent = client_config.user_agent headers["User-Agent"] = user_agent end @@ -635,11 +642,26 @@ module YoutubeAPI LOGGER.trace("YoutubeAPI: ClientConfig: #{client_config}") LOGGER.trace("YoutubeAPI: POST data: #{data}") + invidious_companion_url = CONFIG.invidious_companion + # Send the POST request - body = YT_POOL.client() do |client| - client.post(url, headers: headers, body: data.to_json) do |response| - self._decompress(response.body_io, response.headers["Content-Encoding"]?) + if invidious_companion_url && endpoint == "/youtubei/v1/player" + begin + body = make_client(URI.parse(invidious_companion_url), + &.post(endpoint, headers: headers, body: data.to_json).body) + rescue + raise InfoException.new("Unable to communicate with Invidious companion.") end + else + body = YT_POOL.client() do |client| + client.post(url, headers: headers, body: data.to_json) do |response| + self._decompress(response.body_io, response.headers["Content-Encoding"]?) + end + end + end + + if body.nil? && invidious_companion_url + raise InfoException.new("Unable to communicate with Invidious companion.") end # Convert result to Hash