Compare commits
6 commits
master
...
experiment
Author | SHA1 | Date | |
---|---|---|---|
|
4b9f987ffb | ||
|
2f3266cd01 | ||
|
46041f5b60 | ||
47237d5718 | |||
7ba95e32b5 | |||
8a3fd5751a |
14 changed files with 106 additions and 136 deletions
|
@ -34,8 +34,8 @@ jobs:
|
||||||
with:
|
with:
|
||||||
images: git.nadeko.net/fijxu/invidious
|
images: git.nadeko.net/fijxu/invidious
|
||||||
tags: |
|
tags: |
|
||||||
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-experimental-,enable=${{ github.ref == format('refs/heads/{0}', 'experimental') }}
|
||||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
type=raw,value=latest-experimental,enable=${{ github.ref == format('refs/heads/{0}', 'experimental') }}
|
||||||
|
|
||||||
- uses: https://code.forgejo.org/docker/build-push-action@v5
|
- uses: https://code.forgejo.org/docker/build-push-action@v5
|
||||||
name: Build images
|
name: Build images
|
||||||
|
|
|
@ -45,8 +45,6 @@ struct ConfigPreferences
|
||||||
property vr_mode : Bool = true
|
property vr_mode : Bool = true
|
||||||
property show_nick : Bool = true
|
property show_nick : Bool = true
|
||||||
property save_player_pos : Bool = false
|
property save_player_pos : Bool = false
|
||||||
property po_token : String = ""
|
|
||||||
property visitor_data : String = ""
|
|
||||||
|
|
||||||
def to_tuple
|
def to_tuple
|
||||||
{% begin %}
|
{% begin %}
|
||||||
|
@ -89,8 +87,6 @@ class Config
|
||||||
|
|
||||||
# Used to tell Invidious it is behind a proxy, so links to resources should be https://
|
# Used to tell Invidious it is behind a proxy, so links to resources should be https://
|
||||||
property https_only : Bool?
|
property https_only : Bool?
|
||||||
# Enable or disable CSP
|
|
||||||
property csp : Bool? = true
|
|
||||||
# HMAC signing key for CSRF tokens and verifying pubsub subscriptions
|
# HMAC signing key for CSRF tokens and verifying pubsub subscriptions
|
||||||
property hmac_key : String = ""
|
property hmac_key : String = ""
|
||||||
# Domain to be used for links to resources on the site where an absolute URL is required
|
# Domain to be used for links to resources on the site where an absolute URL is required
|
||||||
|
|
|
@ -4,6 +4,17 @@ module Invidious::HttpServer
|
||||||
module Utils
|
module Utils
|
||||||
extend self
|
extend self
|
||||||
|
|
||||||
|
@@proxy_alive : Bool = false
|
||||||
|
|
||||||
|
def check_external_proxy
|
||||||
|
begin
|
||||||
|
response = HTTP::Client.get("#{CONFIG.external_videoplayback_proxy}")
|
||||||
|
@@proxy_alive = response.status_code == 200
|
||||||
|
rescue
|
||||||
|
@@proxy_alive = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def proxy_video_url(raw_url : String, *, region : String? = nil, absolute : Bool = false)
|
def proxy_video_url(raw_url : String, *, region : String? = nil, absolute : Bool = false)
|
||||||
url = URI.parse(raw_url)
|
url = URI.parse(raw_url)
|
||||||
|
|
||||||
|
@ -14,7 +25,11 @@ module Invidious::HttpServer
|
||||||
url.query_params = params
|
url.query_params = params
|
||||||
|
|
||||||
if absolute
|
if absolute
|
||||||
return "#{HOST_URL}#{url.request_target}"
|
if @@proxy_alive
|
||||||
|
return "#{CONFIG.external_videoplayback_proxy}#{url.request_target}"
|
||||||
|
else
|
||||||
|
return "#{HOST_URL}#{url.request_target}"
|
||||||
|
end
|
||||||
else
|
else
|
||||||
return url.request_target
|
return url.request_target
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,7 @@ class Invidious::Jobs::CheckExternalProxy < Invidious::Jobs::BaseJob
|
||||||
|
|
||||||
def begin
|
def begin
|
||||||
loop do
|
loop do
|
||||||
Invidious::Routes::API::Manifest.check_external_proxy
|
HttpServer::Utils.check_external_proxy
|
||||||
LOGGER.info("CheckExternalProxy: Done, sleeping for 1 minute")
|
LOGGER.info("CheckExternalProxy: Done, sleeping for 1 minute")
|
||||||
sleep 1.minutes
|
sleep 1.minutes
|
||||||
Fiber.yield
|
Fiber.yield
|
||||||
|
|
|
@ -1,15 +1,4 @@
|
||||||
module Invidious::Routes::API::Manifest
|
module Invidious::Routes::API::Manifest
|
||||||
@@proxy_alive : Bool = false
|
|
||||||
|
|
||||||
def self.check_external_proxy
|
|
||||||
begin
|
|
||||||
response = HTTP::Client.get("#{CONFIG.external_videoplayback_proxy}")
|
|
||||||
@@proxy_alive = response.status_code == 200
|
|
||||||
rescue
|
|
||||||
@@proxy_alive = false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# /api/manifest/dash/id/:id
|
# /api/manifest/dash/id/:id
|
||||||
def self.get_dash_video_id(env)
|
def self.get_dash_video_id(env)
|
||||||
env.response.headers.add("Access-Control-Allow-Origin", "*")
|
env.response.headers.add("Access-Control-Allow-Origin", "*")
|
||||||
|
@ -38,36 +27,21 @@ module Invidious::Routes::API::Manifest
|
||||||
haltf env, status_code: response.status_code
|
haltf env, status_code: response.status_code
|
||||||
end
|
end
|
||||||
|
|
||||||
manifest = response.body
|
# Proxy URLs for video playback on invidious.
|
||||||
|
# Other API clients can get the original URLs by omiting `local=true`.
|
||||||
manifest = manifest.gsub(/<BaseURL>[^<]+<\/BaseURL>/) do |baseurl|
|
manifest = response.body.gsub(/<BaseURL>[^<]+<\/BaseURL>/) do |baseurl|
|
||||||
url = baseurl.lchop("<BaseURL>")
|
url = baseurl.lchop("<BaseURL>").rchop("</BaseURL>")
|
||||||
url = url.rchop("</BaseURL>")
|
url = HttpServer::Utils.proxy_video_url(url, absolute: true) if local
|
||||||
|
|
||||||
if local
|
|
||||||
uri = URI.parse(url)
|
|
||||||
if @@proxy_alive
|
|
||||||
url = "#{CONFIG.external_videoplayback_proxy}#{uri.request_target}host/#{uri.host}/"
|
|
||||||
else
|
|
||||||
url = "#{HOST_URL}#{uri.request_target}host/#{uri.host}/"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
"<BaseURL>#{url}</BaseURL>"
|
"<BaseURL>#{url}</BaseURL>"
|
||||||
end
|
end
|
||||||
|
|
||||||
return manifest
|
return manifest
|
||||||
end
|
end
|
||||||
|
|
||||||
adaptive_fmts = video.adaptive_fmts
|
# Ditto, only proxify URLs if `local=true` is used
|
||||||
|
|
||||||
if local
|
if local
|
||||||
adaptive_fmts.each do |fmt|
|
video.adaptive_fmts.each do |fmt|
|
||||||
if @@proxy_alive
|
fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s, absolute: true))
|
||||||
fmt["url"] = JSON::Any.new("#{CONFIG.external_videoplayback_proxy}#{URI.parse(fmt["url"].as_s).request_target}")
|
|
||||||
else
|
|
||||||
fmt["url"] = JSON::Any.new("#{HOST_URL}#{URI.parse(fmt["url"].as_s).request_target}")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -203,8 +177,9 @@ module Invidious::Routes::API::Manifest
|
||||||
manifest = response.body
|
manifest = response.body
|
||||||
|
|
||||||
if local
|
if local
|
||||||
manifest = manifest.gsub(/^https:\/\/\w+---.{11}\.c\.youtube\.com[^\n]*/m) do |match|
|
manifest = manifest.gsub(/^https:\/\/[^\n"]*/m) do |match|
|
||||||
path = URI.parse(match).path
|
uri = URI.parse(match)
|
||||||
|
path = uri.path
|
||||||
|
|
||||||
path = path.lchop("/videoplayback/")
|
path = path.lchop("/videoplayback/")
|
||||||
path = path.rchop("/")
|
path = path.rchop("/")
|
||||||
|
@ -233,7 +208,7 @@ module Invidious::Routes::API::Manifest
|
||||||
raw_params["fvip"] = fvip["fvip"]
|
raw_params["fvip"] = fvip["fvip"]
|
||||||
end
|
end
|
||||||
|
|
||||||
raw_params["local"] = "true"
|
raw_params["host"] = uri.host.not_nil!
|
||||||
|
|
||||||
"#{HOST_URL}/videoplayback?#{raw_params}"
|
"#{HOST_URL}/videoplayback?#{raw_params}"
|
||||||
end
|
end
|
||||||
|
|
|
@ -157,10 +157,12 @@ module Invidious::Routes::Embed
|
||||||
adaptive_fmts = video.adaptive_fmts
|
adaptive_fmts = video.adaptive_fmts
|
||||||
|
|
||||||
if params.local
|
if params.local
|
||||||
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
|
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
|
||||||
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Always proxy DASH streams, otherwise youtube CORS headers will prevent playback
|
||||||
|
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
|
||||||
|
|
||||||
video_streams = video.video_streams
|
video_streams = video.video_streams
|
||||||
audio_streams = video.audio_streams
|
audio_streams = video.audio_streams
|
||||||
|
|
||||||
|
|
|
@ -86,12 +86,6 @@ module Invidious::Routes::PreferencesRoute
|
||||||
show_nick ||= "off"
|
show_nick ||= "off"
|
||||||
show_nick = show_nick == "on"
|
show_nick = show_nick == "on"
|
||||||
|
|
||||||
po_token = env.params.body["po_token"]?.try &.as(String)
|
|
||||||
po_token ||= CONFIG.default_user_preferences.po_token
|
|
||||||
|
|
||||||
visitor_data = env.params.body["visitor_data"]?.try &.as(String)
|
|
||||||
visitor_data ||= CONFIG.default_user_preferences.visitor_data
|
|
||||||
|
|
||||||
comments = [] of String
|
comments = [] of String
|
||||||
2.times do |i|
|
2.times do |i|
|
||||||
comments << (env.params.body["comments[#{i}]"]?.try &.as(String) || CONFIG.default_user_preferences.comments[i])
|
comments << (env.params.body["comments[#{i}]"]?.try &.as(String) || CONFIG.default_user_preferences.comments[i])
|
||||||
|
@ -186,8 +180,6 @@ module Invidious::Routes::PreferencesRoute
|
||||||
vr_mode: vr_mode,
|
vr_mode: vr_mode,
|
||||||
show_nick: show_nick,
|
show_nick: show_nick,
|
||||||
save_player_pos: save_player_pos,
|
save_player_pos: save_player_pos,
|
||||||
po_token: po_token,
|
|
||||||
visitor_data: visitor_data,
|
|
||||||
}.to_json)
|
}.to_json)
|
||||||
|
|
||||||
if user = env.get? "user"
|
if user = env.get? "user"
|
||||||
|
|
|
@ -3,9 +3,6 @@
|
||||||
module Invidious::Routes::Watch
|
module Invidious::Routes::Watch
|
||||||
def self.handle(env)
|
def self.handle(env)
|
||||||
locale = env.get("preferences").as(Preferences).locale
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
user_po_token = env.get("preferences").as(Preferences).po_token
|
|
||||||
user_visitor_data = env.get("preferences").as(Preferences).visitor_data
|
|
||||||
|
|
||||||
region = env.params.query["region"]?
|
region = env.params.query["region"]?
|
||||||
|
|
||||||
if env.params.query.to_s.includes?("%20") || env.params.query.to_s.includes?("+")
|
if env.params.query.to_s.includes?("%20") || env.params.query.to_s.includes?("+")
|
||||||
|
@ -55,7 +52,7 @@ module Invidious::Routes::Watch
|
||||||
env.params.query.delete_all("listen")
|
env.params.query.delete_all("listen")
|
||||||
|
|
||||||
begin
|
begin
|
||||||
video = get_video(id, region: params.region, po_token: user_po_token, visitor_data: user_visitor_data)
|
video = get_video(id, region: params.region)
|
||||||
rescue ex : NotFoundException
|
rescue ex : NotFoundException
|
||||||
LOGGER.error("get_video not found: #{id} : #{ex.message}")
|
LOGGER.error("get_video not found: #{id} : #{ex.message}")
|
||||||
return error_template(404, ex)
|
return error_template(404, ex)
|
||||||
|
@ -131,10 +128,12 @@ module Invidious::Routes::Watch
|
||||||
end
|
end
|
||||||
|
|
||||||
if params.local
|
if params.local
|
||||||
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
|
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
|
||||||
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Always proxy DASH streams, otherwise youtube CORS headers will prevent playback
|
||||||
|
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
|
||||||
|
|
||||||
video_streams = video.video_streams
|
video_streams = video.video_streams
|
||||||
audio_streams = video.audio_streams
|
audio_streams = video.audio_streams
|
||||||
|
|
||||||
|
|
|
@ -57,10 +57,6 @@ struct Preferences
|
||||||
property volume : Int32 = CONFIG.default_user_preferences.volume
|
property volume : Int32 = CONFIG.default_user_preferences.volume
|
||||||
property save_player_pos : Bool = CONFIG.default_user_preferences.save_player_pos
|
property save_player_pos : Bool = CONFIG.default_user_preferences.save_player_pos
|
||||||
|
|
||||||
@[YAML::Field(converter: Preferences::ProcessString)]
|
|
||||||
property po_token : String = ""
|
|
||||||
property visitor_data : String = ""
|
|
||||||
|
|
||||||
module BoolToString
|
module BoolToString
|
||||||
def self.to_json(value : String, json : JSON::Builder)
|
def self.to_json(value : String, json : JSON::Builder)
|
||||||
json.string value
|
json.string value
|
||||||
|
|
|
@ -294,7 +294,7 @@ struct Video
|
||||||
predicate_bool upcoming, isUpcoming
|
predicate_bool upcoming, isUpcoming
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_video(id, refresh = true, region = nil, force_refresh = false, po_token = "", visitor_data = nil)
|
def get_video(id, refresh = true, region = nil, force_refresh = false)
|
||||||
if (video = Invidious::Database::Videos.select(id)) && !region
|
if (video = Invidious::Database::Videos.select(id)) && !region
|
||||||
# If record was last updated over 10 minutes ago, or video has since premiered,
|
# If record was last updated over 10 minutes ago, or video has since premiered,
|
||||||
# refresh (expire param in response lasts for 6 hours)
|
# refresh (expire param in response lasts for 6 hours)
|
||||||
|
@ -304,7 +304,7 @@ def get_video(id, refresh = true, region = nil, force_refresh = false, po_token
|
||||||
force_refresh ||
|
force_refresh ||
|
||||||
video.schema_version != Video::SCHEMA_VERSION # cache control
|
video.schema_version != Video::SCHEMA_VERSION # cache control
|
||||||
begin
|
begin
|
||||||
video = fetch_video(id, region, po_token, visitor_data)
|
video = fetch_video(id, region)
|
||||||
Invidious::Database::Videos.insert(video)
|
Invidious::Database::Videos.insert(video)
|
||||||
rescue ex
|
rescue ex
|
||||||
Invidious::Database::Videos.delete(id)
|
Invidious::Database::Videos.delete(id)
|
||||||
|
@ -312,7 +312,7 @@ def get_video(id, refresh = true, region = nil, force_refresh = false, po_token
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
video = fetch_video(id, region, po_token, visitor_data)
|
video = fetch_video(id, region)
|
||||||
Invidious::Database::Videos.insert(video) if !region
|
Invidious::Database::Videos.insert(video) if !region
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -320,11 +320,11 @@ def get_video(id, refresh = true, region = nil, force_refresh = false, po_token
|
||||||
rescue DB::Error
|
rescue DB::Error
|
||||||
# Avoid common `DB::PoolRetryAttemptsExceeded` error and friends
|
# Avoid common `DB::PoolRetryAttemptsExceeded` error and friends
|
||||||
# Note: All DB errors inherit from `DB::Error`
|
# Note: All DB errors inherit from `DB::Error`
|
||||||
return fetch_video(id, region, po_token, visitor_data)
|
return fetch_video(id, region)
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_video(id, region, po_token, visitor_data)
|
def fetch_video(id, region)
|
||||||
info = extract_video_info(video_id: id, po_token: po_token, visitor_data: visitor_data)
|
info = extract_video_info(video_id: id, level: 0)
|
||||||
|
|
||||||
if reason = info["reason"]?
|
if reason = info["reason"]?
|
||||||
if reason == "Video unavailable"
|
if reason == "Video unavailable"
|
||||||
|
|
|
@ -50,13 +50,22 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def extract_video_info(video_id : String, po_token, visitor_data)
|
def extract_video_info(video_id : String, *, level = 0, client_type = YoutubeAPI::ClientType::WebMobileT2)
|
||||||
|
# Infinite recursion prevention
|
||||||
|
level += 1
|
||||||
|
if level >= 3
|
||||||
|
return {
|
||||||
|
"version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64),
|
||||||
|
"reason" => JSON::Any.new("All counter-measures exhausted"),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
# Init client config for the API
|
# Init client config for the API
|
||||||
client_config = YoutubeAPI::ClientConfig.new
|
client_config = YoutubeAPI::ClientConfig.new
|
||||||
|
client_config.client_type = client_type
|
||||||
|
|
||||||
# Fetch data from the player endpoint
|
# Fetch data from the player endpoint
|
||||||
player_response = YoutubeAPI.player(video_id: video_id, params: "2AMB", client_config: client_config, po_token: po_token, visitor_data: visitor_data)
|
player_response = YoutubeAPI.player(video_id: video_id, params: "2AMB", client_config: client_config)
|
||||||
|
|
||||||
playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s
|
playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s
|
||||||
|
|
||||||
if playability_status != "OK"
|
if playability_status != "OK"
|
||||||
|
@ -65,10 +74,15 @@ def extract_video_info(video_id : String, po_token, visitor_data)
|
||||||
reason ||= subreason.try &.[]("runs").as_a.map(&.[]("text")).join("")
|
reason ||= subreason.try &.[]("runs").as_a.map(&.[]("text")).join("")
|
||||||
reason ||= player_response.dig("playabilityStatus", "reason").as_s
|
reason ||= player_response.dig("playabilityStatus", "reason").as_s
|
||||||
|
|
||||||
# Stop here if video is not a scheduled livestream or
|
# Show the playability status in the reason message
|
||||||
# for LOGIN_REQUIRED when videoDetails element is not found because retrying won't help
|
reason = "#{reason} (#{playability_status})"
|
||||||
if !{"LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status) ||
|
|
||||||
|
if (playability_status == "UNPLAYABLE" && reason.includes?("Get the YouTube app")) || reason.includes?("protect our community")
|
||||||
|
return extract_video_info(video_id: video_id, level: level, client_type: YoutubeAPI::ClientType::IOS)
|
||||||
|
elsif !{"LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status) ||
|
||||||
playability_status == "LOGIN_REQUIRED" && !player_response.dig?("videoDetails")
|
playability_status == "LOGIN_REQUIRED" && !player_response.dig?("videoDetails")
|
||||||
|
# Stop here if video is not a scheduled livestream or
|
||||||
|
# for LOGIN_REQUIRED when videoDetails element is not found because retrying won't help
|
||||||
return {
|
return {
|
||||||
"version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64),
|
"version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64),
|
||||||
"reason" => JSON::Any.new(reason),
|
"reason" => JSON::Any.new(reason),
|
||||||
|
@ -92,7 +106,7 @@ def extract_video_info(video_id : String, po_token, visitor_data)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Don't fetch the next endpoint if the video is unavailable.
|
# Don't fetch the next endpoint if the video is unavailable.
|
||||||
if {"OK", "LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status)
|
if {"OK", "LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED", "UNPLAYABLE"}.any?(playability_status)
|
||||||
next_response = YoutubeAPI.next({"videoId": video_id, "params": ""})
|
next_response = YoutubeAPI.next({"videoId": video_id, "params": ""})
|
||||||
player_response = player_response.merge(next_response)
|
player_response = player_response.merge(next_response)
|
||||||
end
|
end
|
||||||
|
@ -102,24 +116,14 @@ def extract_video_info(video_id : String, po_token, visitor_data)
|
||||||
|
|
||||||
new_player_response = nil
|
new_player_response = nil
|
||||||
|
|
||||||
# Don't use Android client if po_token is passed because po_token doesn't
|
if reason || DECRYPT_FUNCTION.nil? || CONFIG.po_token.nil?
|
||||||
# work for Android client.
|
client_config.client_type = YoutubeAPI::ClientType::IOS
|
||||||
if reason.nil? && CONFIG.po_token.nil?
|
new_player_response = try_fetch_streaming_data(video_id, client_config)
|
||||||
# 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, po_token, visitor_data)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Last hope
|
if reason && !DECRYPT_FUNCTION.nil? && CONFIG.po_token.nil?
|
||||||
# Only trigger if reason found and po_token or didn't work wth Android client.
|
|
||||||
# TvHtml5ScreenEmbed now requires sig helper for it to work but po_token is not required
|
|
||||||
# if the IP address is not blocked.
|
|
||||||
if CONFIG.po_token && reason || CONFIG.po_token.nil? && new_player_response.nil?
|
|
||||||
client_config.client_type = YoutubeAPI::ClientType::TvHtml5ScreenEmbed
|
client_config.client_type = YoutubeAPI::ClientType::TvHtml5ScreenEmbed
|
||||||
new_player_response = try_fetch_streaming_data(video_id, client_config, po_token, visitor_data)
|
new_player_response = try_fetch_streaming_data(video_id, client_config)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Replace player response and reset reason
|
# Replace player response and reset reason
|
||||||
|
@ -140,7 +144,7 @@ def extract_video_info(video_id : String, po_token, visitor_data)
|
||||||
if streaming_data = player_response["streamingData"]?
|
if streaming_data = player_response["streamingData"]?
|
||||||
%w[formats adaptiveFormats].each do |key|
|
%w[formats adaptiveFormats].each do |key|
|
||||||
streaming_data.as_h[key]?.try &.as_a.each do |format|
|
streaming_data.as_h[key]?.try &.as_a.each do |format|
|
||||||
format.as_h["url"] = JSON::Any.new(convert_url(format, po_token))
|
format.as_h["url"] = JSON::Any.new(convert_url(format))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -153,9 +157,9 @@ def extract_video_info(video_id : String, po_token, visitor_data)
|
||||||
return params
|
return params
|
||||||
end
|
end
|
||||||
|
|
||||||
def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig, po_token, visitor_data) : Hash(String, JSON::Any)?
|
def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)?
|
||||||
LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.")
|
LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.")
|
||||||
response = YoutubeAPI.player(video_id: id, params: "2AMB", client_config: client_config, po_token: po_token, visitor_data: visitor_data)
|
response = YoutubeAPI.player(video_id: id, params: "2AMB", client_config: client_config)
|
||||||
|
|
||||||
playability_status = response["playabilityStatus"]["status"]
|
playability_status = response["playabilityStatus"]["status"]
|
||||||
LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.")
|
LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.")
|
||||||
|
@ -455,7 +459,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
|
||||||
return params
|
return params
|
||||||
end
|
end
|
||||||
|
|
||||||
private def convert_url(fmt, po_token)
|
private def convert_url(fmt)
|
||||||
if cfr = fmt["signatureCipher"]?.try { |json| HTTP::Params.parse(json.as_s) }
|
if cfr = fmt["signatureCipher"]?.try { |json| HTTP::Params.parse(json.as_s) }
|
||||||
sp = cfr["sp"]
|
sp = cfr["sp"]
|
||||||
url = URI.parse(cfr["url"])
|
url = URI.parse(cfr["url"])
|
||||||
|
@ -470,13 +474,15 @@ private def convert_url(fmt, po_token)
|
||||||
params = url.query_params
|
params = url.query_params
|
||||||
end
|
end
|
||||||
|
|
||||||
n = DECRYPT_FUNCTION.try &.decrypt_nsig(params["n"])
|
if old_n = params["n"]?
|
||||||
params["n"] = n if n
|
n = DECRYPT_FUNCTION.try &.decrypt_nsig(old_n)
|
||||||
|
params["n"] = n if n
|
||||||
|
end
|
||||||
|
|
||||||
if !po_token.empty?
|
if token = CONFIG.po_token
|
||||||
params["pot"] = po_token
|
if {"WEB", "TVHTML5"}.any? { |x| params["c"].starts_with?(x) }
|
||||||
elsif token = CONFIG.po_token
|
params["pot"] = token
|
||||||
params["pot"] = token
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
url.query_params = params
|
url.query_params = params
|
||||||
|
|
|
@ -4,9 +4,6 @@
|
||||||
<% if params.autoplay %>autoplay<% end %>
|
<% if params.autoplay %>autoplay<% end %>
|
||||||
<% if params.video_loop %>loop<% end %>
|
<% if params.video_loop %>loop<% end %>
|
||||||
<% if params.controls %>controls<% end %>>
|
<% if params.controls %>controls<% end %>>
|
||||||
<% if (hlsvp = video.hls_manifest_url) && !CONFIG.disabled?("livestreams") %>
|
|
||||||
<source src="<%= URI.parse(hlsvp).request_target %><% if params.local %>?local=true<% end %>" type="application/x-mpegURL" label="livestream">
|
|
||||||
<% else %>
|
|
||||||
<% if params.listen %>
|
<% if params.listen %>
|
||||||
<% # default to 128k m4a stream
|
<% # default to 128k m4a stream
|
||||||
best_m4a_stream_index = 0
|
best_m4a_stream_index = 0
|
||||||
|
@ -57,6 +54,11 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<% if (hlsvp = video.hls_manifest_url) && !CONFIG.disabled?("livestreams") %>
|
||||||
|
<source src="<%= URI.parse(hlsvp).request_target %><% if params.local %>?local=true<% end %>" type="application/x-mpegURL" label="livestream">
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
<% preferred_captions.each do |caption| %>
|
<% preferred_captions.each do |caption| %>
|
||||||
<track kind="captions" src="/api/v1/captions/<%= video.id %>?label=<%= caption.name %>" label="<%= caption.name %>">
|
<track kind="captions" src="/api/v1/captions/<%= video.id %>?label=<%= caption.name %>" label="<%= caption.name %>">
|
||||||
<% end %>
|
<% end %>
|
||||||
|
@ -64,7 +66,6 @@
|
||||||
<% captions.each do |caption| %>
|
<% captions.each do |caption| %>
|
||||||
<track kind="captions" src="/api/v1/captions/<%= video.id %>?label=<%= caption.name %>" label="<%= caption.name %>">
|
<track kind="captions" src="/api/v1/captions/<%= video.id %>?label=<%= caption.name %>" label="<%= caption.name %>">
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
|
||||||
</video>
|
</video>
|
||||||
|
|
||||||
<script id="player_data" type="application/json">
|
<script id="player_data" type="application/json">
|
||||||
|
|
|
@ -126,16 +126,6 @@
|
||||||
<input name="save_player_pos" id="save_player_pos" type="checkbox" <% if preferences.save_player_pos %>checked<% end %>>
|
<input name="save_player_pos" id="save_player_pos" type="checkbox" <% if preferences.save_player_pos %>checked<% end %>>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
|
||||||
<label for="po_token"><%= translate(locale, "preferences_po_token") %></label>
|
|
||||||
<input name="po_token" id="po_token" type="text" value="<%= preferences.po_token %>">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pure-control-group">
|
|
||||||
<label for="visitor_data"><%= translate(locale, "preferences_visitor_data") %></label>
|
|
||||||
<input name="visitor_data" id="visitor_data" type="text" value="<%= preferences.visitor_data %>">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<legend><%= translate(locale, "preferences_category_visual") %></legend>
|
<legend><%= translate(locale, "preferences_category_visual") %></legend>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
|
|
|
@ -17,7 +17,7 @@ module YoutubeAPI
|
||||||
# For Apple device names, see https://gist.github.com/adamawolf/3048717
|
# For Apple device names, see https://gist.github.com/adamawolf/3048717
|
||||||
# For iOS versions, see https://en.wikipedia.org/wiki/IOS_version_history#Releases,
|
# For iOS versions, see https://en.wikipedia.org/wiki/IOS_version_history#Releases,
|
||||||
# then go to the dedicated article of the major version you want.
|
# then go to the dedicated article of the major version you want.
|
||||||
private IOS_APP_VERSION = "19.32.8"
|
private IOS_APP_VERSION = "19.40.4"
|
||||||
private IOS_USER_AGENT = "com.google.ios.youtube/#{IOS_APP_VERSION} (iPhone14,5; U; CPU iOS 17_6 like Mac OS X;)"
|
private IOS_USER_AGENT = "com.google.ios.youtube/#{IOS_APP_VERSION} (iPhone14,5; U; CPU iOS 17_6 like Mac OS X;)"
|
||||||
private IOS_VERSION = "17.6.1.21G93" # Major.Minor.Patch.Build
|
private IOS_VERSION = "17.6.1.21G93" # Major.Minor.Patch.Build
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ module YoutubeAPI
|
||||||
Web
|
Web
|
||||||
WebEmbeddedPlayer
|
WebEmbeddedPlayer
|
||||||
WebMobile
|
WebMobile
|
||||||
|
WebMobileT2
|
||||||
WebScreenEmbed
|
WebScreenEmbed
|
||||||
|
|
||||||
Android
|
Android
|
||||||
|
@ -48,7 +49,7 @@ module YoutubeAPI
|
||||||
ClientType::Web => {
|
ClientType::Web => {
|
||||||
name: "WEB",
|
name: "WEB",
|
||||||
name_proto: "1",
|
name_proto: "1",
|
||||||
version: "2.20240814.00.00",
|
version: "2.20241010.01.00",
|
||||||
screen: "WATCH_FULL_SCREEN",
|
screen: "WATCH_FULL_SCREEN",
|
||||||
os_name: "Windows",
|
os_name: "Windows",
|
||||||
os_version: WINDOWS_VERSION,
|
os_version: WINDOWS_VERSION,
|
||||||
|
@ -66,11 +67,19 @@ module YoutubeAPI
|
||||||
ClientType::WebMobile => {
|
ClientType::WebMobile => {
|
||||||
name: "MWEB",
|
name: "MWEB",
|
||||||
name_proto: "2",
|
name_proto: "2",
|
||||||
version: "2.20240813.02.00",
|
version: "2.20241010.02.00",
|
||||||
os_name: "Android",
|
os_name: "Android",
|
||||||
os_version: ANDROID_VERSION,
|
os_version: ANDROID_VERSION,
|
||||||
platform: "MOBILE",
|
platform: "MOBILE",
|
||||||
},
|
},
|
||||||
|
ClientType::WebMobileT2 => {
|
||||||
|
name: "MWEB_TIER_2",
|
||||||
|
name_proto: "27",
|
||||||
|
version: "9.20241010",
|
||||||
|
os_name: "iPhone",
|
||||||
|
os_version: IOS_VERSION,
|
||||||
|
platform: "MOBILE",
|
||||||
|
},
|
||||||
ClientType::WebScreenEmbed => {
|
ClientType::WebScreenEmbed => {
|
||||||
name: "WEB",
|
name: "WEB",
|
||||||
name_proto: "1",
|
name_proto: "1",
|
||||||
|
@ -198,8 +207,6 @@ module YoutubeAPI
|
||||||
# (this is passed as the `gl` parameter).
|
# (this is passed as the `gl` parameter).
|
||||||
property region : String | Nil
|
property region : String | Nil
|
||||||
|
|
||||||
@@visitor_data : String | Nil
|
|
||||||
|
|
||||||
# Initialization function
|
# Initialization function
|
||||||
def initialize(
|
def initialize(
|
||||||
*,
|
*,
|
||||||
|
@ -322,9 +329,7 @@ module YoutubeAPI
|
||||||
client_context["client"]["platform"] = platform
|
client_context["client"]["platform"] = platform
|
||||||
end
|
end
|
||||||
|
|
||||||
if !@@visitor_data.not_nil!.empty?
|
if CONFIG.visitor_data.is_a?(String)
|
||||||
client_context["client"]["visitorData"] = @@visitor_data.not_nil!
|
|
||||||
elsif CONFIG.visitor_data.is_a?(String)
|
|
||||||
client_context["client"]["visitorData"] = CONFIG.visitor_data.as(String)
|
client_context["client"]["visitorData"] = CONFIG.visitor_data.as(String)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -459,13 +464,8 @@ module YoutubeAPI
|
||||||
video_id : String,
|
video_id : String,
|
||||||
*, # Force the following parameters to be passed by name
|
*, # Force the following parameters to be passed by name
|
||||||
params : String,
|
params : String,
|
||||||
client_config : ClientConfig | Nil = nil,
|
client_config : ClientConfig | Nil = nil
|
||||||
po_token : String,
|
|
||||||
visitor_data : String | Nil
|
|
||||||
)
|
)
|
||||||
if visitor_data
|
|
||||||
@@visitor_data = visitor_data
|
|
||||||
end
|
|
||||||
# Playback context, separate because it can be different between clients
|
# Playback context, separate because it can be different between clients
|
||||||
playback_ctx = {
|
playback_ctx = {
|
||||||
"html5Preference" => "HTML5_PREF_WANTS",
|
"html5Preference" => "HTML5_PREF_WANTS",
|
||||||
|
@ -491,7 +491,7 @@ module YoutubeAPI
|
||||||
"contentPlaybackContext" => playback_ctx,
|
"contentPlaybackContext" => playback_ctx,
|
||||||
},
|
},
|
||||||
"serviceIntegrityDimensions" => {
|
"serviceIntegrityDimensions" => {
|
||||||
"poToken" => po_token || CONFIG.po_token,
|
"poToken" => CONFIG.po_token,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -605,7 +605,7 @@ module YoutubeAPI
|
||||||
def _post_json(
|
def _post_json(
|
||||||
endpoint : String,
|
endpoint : String,
|
||||||
data : Hash,
|
data : Hash,
|
||||||
client_config : ClientConfig | Nil,
|
client_config : ClientConfig | Nil
|
||||||
) : Hash(String, JSON::Any)
|
) : Hash(String, JSON::Any)
|
||||||
# Use the default client config if nil is passed
|
# Use the default client config if nil is passed
|
||||||
client_config ||= DEFAULT_CLIENT_CONFIG
|
client_config ||= DEFAULT_CLIENT_CONFIG
|
||||||
|
@ -625,9 +625,7 @@ module YoutubeAPI
|
||||||
headers["User-Agent"] = user_agent
|
headers["User-Agent"] = user_agent
|
||||||
end
|
end
|
||||||
|
|
||||||
if !@@visitor_data.not_nil!.empty?
|
if CONFIG.visitor_data.is_a?(String)
|
||||||
headers["X-Goog-Visitor-Id"] = @@visitor_data.not_nil!
|
|
||||||
elsif CONFIG.visitor_data.is_a?(String)
|
|
||||||
headers["X-Goog-Visitor-Id"] = CONFIG.visitor_data.as(String)
|
headers["X-Goog-Visitor-Id"] = CONFIG.visitor_data.as(String)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue