Skip to content

Commit 1aa154b

Browse files
committed
separate invidious_companion logic + better config.yaml config
1 parent ff3305d commit 1aa154b

File tree

9 files changed

+119
-65
lines changed

9 files changed

+119
-65
lines changed

src/invidious/config.cr

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,28 @@ end
6767
class Config
6868
include YAML::Serializable
6969

70+
module URIArrayConverter
71+
def self.to_yaml(values : Array(URI), yaml : YAML::Nodes::Builder)
72+
yaml.sequence do
73+
values.each { |v| yaml.scalar v.to_s }
74+
end
75+
end
76+
77+
def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Array(URI)
78+
if node.is_a?(YAML::Nodes::Sequence)
79+
node.map do |child|
80+
unless child.is_a?(YAML::Nodes::Scalar)
81+
node.raise "Expected scalar, not #{child.class}"
82+
end
83+
84+
URI.parse(child.value)
85+
end
86+
else
87+
node.raise "Expected sequence, not #{node.class}"
88+
end
89+
end
90+
end
91+
7092
# Number of threads to use for crawling videos from channels (for updating subscriptions)
7193
property channel_threads : Int32 = 1
7294
# Time interval between two executions of the job that crawls channel videos (subscriptions update).
@@ -152,10 +174,11 @@ class Config
152174
property po_token : String? = nil
153175

154176
# Invidious companion
155-
property invidious_companion : Array(String)? = nil
177+
@[YAML::Field(converter: Config::URIArrayConverter)]
178+
property invidious_companion : Array(URI) = [] of URI
156179

157180
# Invidious companion API key
158-
property invidious_companion_key : String? = nil
181+
property invidious_companion_key : String = ""
159182

160183
# Saved cookies in "name1=value1; name2=value2..." format
161184
@[YAML::Field(converter: Preferences::StringToCookies)]
@@ -228,7 +251,7 @@ class Config
228251
end
229252
{% end %}
230253

231-
if CONFIG.invidious_companion
254+
if !CONFIG.invidious_companion.empty?
232255
# invidious_companion and signature_server can't work together
233256
if CONFIG.signature_server
234257
puts "Config: You can not run inv_sig_helper and invidious_companion at the same time."

src/invidious/routes/api/manifest.cr

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
module Invidious::Routes::API::Manifest
22
# /api/manifest/dash/id/:id
33
def self.get_dash_video_id(env)
4+
if !CONFIG.invidious_companion.empty?
5+
return error_template(403, "This endpoint is not permitted because it is handled by Invidious companion.")
6+
end
7+
48
env.response.headers.add("Access-Control-Allow-Origin", "*")
59
env.response.content_type = "application/dash+xml"
610

@@ -20,10 +24,6 @@ module Invidious::Routes::API::Manifest
2024
haltf env, status_code: 403
2125
end
2226

23-
if local && CONFIG.invidious_companion
24-
return env.redirect "#{video.invidious_companion["baseUrl"].as_s}#{env.request.path}?#{env.request.query}"
25-
end
26-
2727
if dashmpd = video.dash_manifest_url
2828
response = YT_POOL.client &.get(URI.parse(dashmpd).request_target)
2929

src/invidious/routes/embed.cr

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,13 @@ module Invidious::Routes::Embed
201201
return env.redirect url
202202
end
203203

204+
if (!CONFIG.invidious_companion.empty? && (preferences.local || preferences.quality == "dash"))
205+
env.response.headers["Content-Security-Policy"] =
206+
env.response.headers["Content-Security-Policy"]
207+
.gsub("media-src", "media-src " + video.invidious_companion.not_nil!["baseUrl"].as_s)
208+
.gsub("connect-src", "connect-src " + video.invidious_companion.not_nil!["baseUrl"].as_s)
209+
end
210+
204211
rendered "embed"
205212
end
206213
end

src/invidious/routes/video_playback.cr

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,10 @@ module Invidious::Routes::VideoPlayback
253253
# YouTube /videoplayback links expire after 6 hours,
254254
# so we have a mechanism here to redirect to the latest version
255255
def self.latest_version(env)
256+
if !CONFIG.invidious_companion.empty? && CONFIG.disabled?("downloads")
257+
return error_template(403, "This endpoint is not permitted because it is handled by Invidious companion.")
258+
end
259+
256260
id = env.params.query["id"]?
257261
itag = env.params.query["itag"]?.try &.to_i?
258262

@@ -294,8 +298,8 @@ module Invidious::Routes::VideoPlayback
294298
end
295299

296300
if local
297-
if (CONFIG.invidious_companion)
298-
return env.redirect "#{video.invidious_companion["baseUrl"].as_s}#{env.request.path}?#{env.request.query}"
301+
if (!CONFIG.invidious_companion.empty?)
302+
return env.redirect "#{video.invidious_companion.not_nil!["baseUrl"].as_s}#{env.request.path}?#{env.request.query}"
299303
end
300304
url = URI.parse(url).request_target.not_nil!
301305
url += "&title=#{URI.encode_www_form(title, space_to_plus: false)}" if title

src/invidious/routes/watch.cr

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,11 +190,11 @@ module Invidious::Routes::Watch
190190
captions: video.captions
191191
)
192192

193-
if (CONFIG.invidious_companion && (preferences.local || preferences.quality == "dash"))
193+
if (!CONFIG.invidious_companion.empty? && (preferences.local || preferences.quality == "dash"))
194194
env.response.headers["Content-Security-Policy"] =
195195
env.response.headers["Content-Security-Policy"]
196-
.gsub("media-src", "media-src " + video.invidious_companion["baseUrl"].as_s)
197-
.gsub("connect-src", "connect-src " + video.invidious_companion["baseUrl"].as_s)
196+
.gsub("media-src", "media-src " + video.invidious_companion.not_nil!["baseUrl"].as_s)
197+
.gsub("connect-src", "connect-src " + video.invidious_companion.not_nil!["baseUrl"].as_s)
198198
end
199199

200200
templated "watch"

src/invidious/videos.cr

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ struct Video
1515
# NOTE: don't forget to bump this number if any change is made to
1616
# the `params` structure in videos/parser.cr!!!
1717
#
18-
SCHEMA_VERSION = 2
18+
SCHEMA_VERSION = 3
1919

2020
property id : String
2121

@@ -192,8 +192,8 @@ struct Video
192192
}
193193
end
194194

195-
def invidious_companion : Hash(String, JSON::Any)
196-
info["invidiousCompanion"].try &.as_h
195+
def invidious_companion : Hash(String, JSON::Any)?
196+
info["invidiousCompanion"]?.try &.as_h
197197
end
198198

199199
# Macros defining getters/setters for various types of data

src/invidious/videos/parser.cr

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def extract_video_info(video_id : String)
100100
params = parse_video_info(video_id, player_response)
101101
params["reason"] = JSON::Any.new(reason) if reason
102102

103-
if CONFIG.invidious_companion.nil?
103+
if !CONFIG.invidious_companion.empty?
104104
new_player_response = nil
105105

106106
# Don't use Android test suite client if po_token is passed because po_token doesn't
@@ -126,7 +126,7 @@ def extract_video_info(video_id : String)
126126
end
127127
end
128128

129-
{"captions", "playabilityStatus", "playerConfig", "storyboards"}.each do |f|
129+
{"captions", "playabilityStatus", "playerConfig", "storyboards", "invidiousCompanion"}.each do |f|
130130
params[f] = player_response[f] if player_response[f]?
131131
end
132132

@@ -141,10 +141,6 @@ def extract_video_info(video_id : String)
141141
params["streamingData"] = streaming_data
142142
end
143143

144-
if CONFIG.invidious_companion
145-
params["invidiousCompanion"] = player_response["invidiousCompanion"]
146-
end
147-
148144
# Data structure version, for cache control
149145
params["version"] = JSON::Any.new(Video::SCHEMA_VERSION.to_i64)
150146

@@ -453,12 +449,11 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
453449
# Music section
454450
"music" => JSON.parse(music_list.to_json),
455451
# Author infos
456-
"author" => JSON::Any.new(author || ""),
457-
"ucid" => JSON::Any.new(ucid || ""),
458-
"authorThumbnail" => JSON::Any.new(author_thumbnail.try &.as_s || ""),
459-
"authorVerified" => JSON::Any.new(author_verified || false),
460-
"subCountText" => JSON::Any.new(subs_text || "-"),
461-
"invidiousCompanion" => JSON::Any.new(subs_text),
452+
"author" => JSON::Any.new(author || ""),
453+
"ucid" => JSON::Any.new(ucid || ""),
454+
"authorThumbnail" => JSON::Any.new(author_thumbnail.try &.as_s || ""),
455+
"authorVerified" => JSON::Any.new(author_verified || false),
456+
"subCountText" => JSON::Any.new(subs_text || "-"),
462457
}
463458

464459
return params

src/invidious/views/components/player.ecr

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
audio_streams.each_with_index do |fmt, i|
2323
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
2424
src_url += "&local=true" if params.local
25-
src_url = video.invidious_companion["baseUrl"].as_s + src_url if (CONFIG.invidious_companion && params.local)
25+
src_url = video.invidious_companion.not_nil!["baseUrl"].as_s + src_url if (!CONFIG.invidious_companion.empty? && params.local)
2626
2727
bitrate = fmt["bitrate"]
2828
mimetype = HTML.escape(fmt["mimeType"].as_s)
@@ -37,7 +37,7 @@
3737
<% else %>
3838
<% if params.quality == "dash"
3939
src_url = "/api/manifest/dash/id/" + video.id + "?local=true&unique_res=1"
40-
src_url = video.invidious_companion["baseUrl"].as_s + src_url if (CONFIG.invidious_companion)
40+
src_url = video.invidious_companion.not_nil!["baseUrl"].as_s + src_url if (!CONFIG.invidious_companion.empty?)
4141
%>
4242
<source src="<%= src_url %>" type='application/dash+xml' label="dash">
4343
<% end %>
@@ -48,7 +48,7 @@
4848
fmt_stream.each_with_index do |fmt, i|
4949
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
5050
src_url += "&local=true" if params.local
51-
src_url = video.invidious_companion["baseUrl"].as_s + src_url if (CONFIG.invidious_companion && params.local)
51+
src_url = video.invidious_companion.not_nil!["baseUrl"].as_s + src_url if (!CONFIG.invidious_companion.empty? && params.local)
5252
5353
quality = fmt["quality"]
5454
mimetype = HTML.escape(fmt["mimeType"].as_s)

src/invidious/yt_backend/youtube_api.cr

Lines changed: 60 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,11 @@ module YoutubeAPI
500500
data["params"] = params
501501
end
502502

503-
return self._post_json("/youtubei/v1/player", data, client_config)
503+
if !CONFIG.invidious_companion.empty?
504+
return self._post_invidious_companion("/youtubei/v1/player", data)
505+
else
506+
return self._post_json("/youtubei/v1/player", data, client_config)
507+
end
504508
end
505509

506510
####################################################################
@@ -615,19 +619,12 @@ module YoutubeAPI
615619

616620
headers = HTTP::Headers{
617621
"Content-Type" => "application/json; charset=UTF-8",
622+
"Accept-Encoding" => "gzip, deflate",
618623
"x-goog-api-format-version" => "2",
619624
"x-youtube-client-name" => client_config.name_proto,
620625
"x-youtube-client-version" => client_config.version,
621626
}
622627

623-
if CONFIG.invidious_companion && endpoint == "/youtubei/v1/player"
624-
headers["Authorization"] = "Bearer " + CONFIG.hmac_key
625-
end
626-
627-
if !CONFIG.invidious_companion
628-
headers["Accept-Encoding"] = "gzip, deflate"
629-
end
630-
631628
if user_agent = client_config.user_agent
632629
headers["User-Agent"] = user_agent
633630
end
@@ -641,37 +638,18 @@ module YoutubeAPI
641638
LOGGER.trace("YoutubeAPI: ClientConfig: #{client_config}")
642639
LOGGER.trace("YoutubeAPI: POST data: #{data}")
643640

644-
invidious_companion_urls = CONFIG.invidious_companion
645-
646641
# Send the POST request
647-
if invidious_companion_urls && endpoint == "/youtubei/v1/player"
648-
begin
649-
invidious_companion_response = make_client(URI.parse(invidious_companion_urls.sample),
650-
&.post(endpoint, headers: headers, body: data.to_json))
651-
body = invidious_companion_response.body
652-
if (invidious_companion_response.status_code != 200)
653-
raise Exception.new("status code: " + invidious_companion_response.status_code.to_s + " and body: " + body)
654-
end
655-
rescue ex
656-
raise InfoException.new("Error while communicating with Invidious companion: " + (ex.message || "no extra info found"))
657-
end
658-
else
659-
body = YT_POOL.client() do |client|
660-
client.post(url, headers: headers, body: data.to_json) do |response|
661-
if response.status_code != 200
662-
raise InfoException.new("Error: non 200 status code. Youtube API returned \
663-
status code #{response.status_code}. See <a href=\"https://docs.invidious.io/youtube-errors-explained/\"> \
664-
https://docs.invidious.io/youtube-errors-explained/</a> for troubleshooting.")
665-
end
666-
self._decompress(response.body_io, response.headers["Content-Encoding"]?)
642+
body = YT_POOL.client() do |client|
643+
client.post(url, headers: headers, body: data.to_json) do |response|
644+
if response.status_code != 200
645+
raise InfoException.new("Error: non 200 status code. Youtube API returned \
646+
status code #{response.status_code}. See <a href=\"https://docs.invidious.io/youtube-errors-explained/\"> \
647+
https://docs.invidious.io/youtube-errors-explained/</a> for troubleshooting.")
667648
end
649+
self._decompress(response.body_io, response.headers["Content-Encoding"]?)
668650
end
669651
end
670652

671-
if body.nil? && CONFIG.invidious_companion
672-
raise InfoException.new("Error while communicating with Invidious companion: no response data.")
673-
end
674-
675653
# Convert result to Hash
676654
initial_data = JSON.parse(body).as_h
677655

@@ -692,6 +670,53 @@ module YoutubeAPI
692670
return initial_data
693671
end
694672

673+
####################################################################
674+
# _post_invidious_companion(endpoint, data)
675+
#
676+
# Internal function that does the actual request to Invidious companion
677+
# and handles errors.
678+
#
679+
# The requested data is an endpoint (URL without the domain part)
680+
# and the data as a Hash object.
681+
#
682+
def _post_invidious_companion(
683+
endpoint : String,
684+
data : Hash
685+
) : Hash(String, JSON::Any)
686+
headers = HTTP::Headers{
687+
"Content-Type" => "application/json; charset=UTF-8",
688+
"Accept-Encoding" => "gzip",
689+
"Authorization" => "Bearer " + CONFIG.invidious_companion_key,
690+
}
691+
692+
# Logging
693+
LOGGER.debug("Invidious companion: Using endpoint: \"#{endpoint}\"")
694+
LOGGER.trace("Invidious companion: POST data: #{data}")
695+
696+
# Send the POST request
697+
698+
begin
699+
response = make_client(CONFIG.invidious_companion.sample,
700+
&.post(endpoint, headers: headers, body: data.to_json))
701+
body = self._decompress(response.body_io, response.headers["Content-Encoding"]?)
702+
if (response.status_code != 200)
703+
raise Exception.new("Error while communicating with Invidious companion: \
704+
status code: " + response.status_code.to_s + " and body: " + body)
705+
end
706+
rescue ex
707+
raise InfoException.new("Error while communicating with Invidious companion: " + (ex.message || "no extra info found"))
708+
end
709+
710+
if body.nil?
711+
raise InfoException.new("Error while communicating with Invidious companion: no response data.")
712+
end
713+
714+
# Convert result to Hash
715+
initial_data = JSON.parse(body).as_h
716+
717+
return initial_data
718+
end
719+
695720
####################################################################
696721
# _decompress(body_io, headers)
697722
#

0 commit comments

Comments
 (0)