mirror of
https://github.com/iv-org/invidious.git
synced 2025-08-21 10:49:02 -05:00
Merge 8ed07a58d43ddc9299de13e915b4672521648c62 into 1ae0f45b0e5dca696986925a06ef4f4b4f43894b
This commit is contained in:
commit
3b5f5a1124
94
spec/streaming_fallback_spec.cr
Normal file
94
spec/streaming_fallback_spec.cr
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
require "spectator"
|
||||||
|
|
||||||
|
# Bring in the helper under test
|
||||||
|
require "../src/invidious/videos/parser.cr"
|
||||||
|
|
||||||
|
Spectator.describe Invidious::Videos::ParserHelpers do
|
||||||
|
def json_any_hash(h : Hash(String, JSON::Any))
|
||||||
|
h
|
||||||
|
end
|
||||||
|
|
||||||
|
def json_any_array(a : Array(JSON::Any))
|
||||||
|
JSON::Any.new(a)
|
||||||
|
end
|
||||||
|
|
||||||
|
def json_any_str(s : String)
|
||||||
|
JSON::Any.new(s)
|
||||||
|
end
|
||||||
|
|
||||||
|
def json_any_obj(h : Hash(String, JSON::Any))
|
||||||
|
JSON::Any.new(h)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "patches formats when primary missing and fallback has usable formats" do
|
||||||
|
primary_sd = {
|
||||||
|
"formats" => JSON::Any.new([] of JSON::Any),
|
||||||
|
"adaptiveFormats" => JSON::Any.new([] of JSON::Any),
|
||||||
|
} of String => JSON::Any
|
||||||
|
|
||||||
|
fallback_sd = {
|
||||||
|
"formats" => JSON::Any.new([
|
||||||
|
JSON::Any.new({"url" => json_any_str("https://example.com/video.mp4")}),
|
||||||
|
] of JSON::Any),
|
||||||
|
"adaptiveFormats" => JSON::Any.new([
|
||||||
|
JSON::Any.new({"url" => json_any_str("https://example.com/audio.m4a")}),
|
||||||
|
] of JSON::Any),
|
||||||
|
} of String => JSON::Any
|
||||||
|
|
||||||
|
res = Invidious::Videos::ParserHelpers.patch_streaming_data_if_missing!(primary_sd, fallback_sd)
|
||||||
|
|
||||||
|
expect(res[:patched_formats]).to be_true
|
||||||
|
expect(res[:patched_adaptive]).to be_true
|
||||||
|
|
||||||
|
# Ensure formats now have a non-empty URL
|
||||||
|
first_fmt = primary_sd["formats"].as_a[0].as_h
|
||||||
|
expect(first_fmt["url"].as_s).to_not be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not overwrite valid primary data" do
|
||||||
|
primary_sd = {
|
||||||
|
"formats" => JSON::Any.new([
|
||||||
|
JSON::Any.new({"url" => json_any_str("https://primary/video.mp4")}),
|
||||||
|
] of JSON::Any),
|
||||||
|
"adaptiveFormats" => JSON::Any.new([
|
||||||
|
JSON::Any.new({"url" => json_any_str("https://primary/audio.m4a")}),
|
||||||
|
] of JSON::Any),
|
||||||
|
} of String => JSON::Any
|
||||||
|
|
||||||
|
fallback_sd = {
|
||||||
|
"formats" => JSON::Any.new([
|
||||||
|
JSON::Any.new({"url" => json_any_str("https://fallback/video.mp4")}),
|
||||||
|
] of JSON::Any),
|
||||||
|
"adaptiveFormats" => JSON::Any.new([
|
||||||
|
JSON::Any.new({"url" => json_any_str("https://fallback/audio.m4a")}),
|
||||||
|
] of JSON::Any),
|
||||||
|
} of String => JSON::Any
|
||||||
|
|
||||||
|
res = Invidious::Videos::ParserHelpers.patch_streaming_data_if_missing!(primary_sd, fallback_sd)
|
||||||
|
|
||||||
|
expect(res[:patched_formats]).to be_false
|
||||||
|
expect(res[:patched_adaptive]).to be_false
|
||||||
|
|
||||||
|
# Primary values should remain
|
||||||
|
expect(primary_sd["formats"].as_a[0].as_h["url"].as_s).to eq("https://primary/video.mp4")
|
||||||
|
expect(primary_sd["adaptiveFormats"].as_a[0].as_h["url"].as_s).to eq("https://primary/audio.m4a")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "handles fallback without formats gracefully" do
|
||||||
|
primary_sd = {
|
||||||
|
"formats" => JSON::Any.new([] of JSON::Any),
|
||||||
|
"adaptiveFormats" => JSON::Any.new([] of JSON::Any),
|
||||||
|
} of String => JSON::Any
|
||||||
|
|
||||||
|
fallback_sd = {
|
||||||
|
"adaptiveFormats" => JSON::Any.new([
|
||||||
|
JSON::Any.new({"url" => json_any_str("https://example.com/audio.m4a")}),
|
||||||
|
] of JSON::Any),
|
||||||
|
} of String => JSON::Any
|
||||||
|
|
||||||
|
res = Invidious::Videos::ParserHelpers.patch_streaming_data_if_missing!(primary_sd, fallback_sd)
|
||||||
|
|
||||||
|
expect(res[:patched_adaptive]).to be_true
|
||||||
|
expect(res[:patched_formats]).to be_false
|
||||||
|
end
|
||||||
|
end
|
@ -299,6 +299,8 @@ module Invidious::Routes::VideoPlayback
|
|||||||
url = fmt.try &.["url"]?.try &.as_s
|
url = fmt.try &.["url"]?.try &.as_s
|
||||||
|
|
||||||
if !url
|
if !url
|
||||||
|
# Extra context for debugging playback errors
|
||||||
|
LOGGER.warn("playback_404: no URL for id=#{id} itag=#{itag.inspect} fmt=#{video.fmt_stream.size} adaptive=#{video.adaptive_fmts.size}")
|
||||||
haltf env, status_code: 404
|
haltf env, status_code: 404
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -58,6 +58,59 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Small helper utilities extracted for testability and clarity (per LLVM policy)
|
||||||
|
module Invidious::Videos::ParserHelpers
|
||||||
|
extend self
|
||||||
|
|
||||||
|
# True if any entry has a usable URL or signatureCipher/cipher that can be converted to a URL
|
||||||
|
def has_usable_stream?(container : JSON::Any?) : Bool
|
||||||
|
return false unless container
|
||||||
|
arr = container.as_a?
|
||||||
|
return false unless arr && arr.size > 0
|
||||||
|
arr.each do |entry|
|
||||||
|
obj = entry.as_h?
|
||||||
|
next unless obj
|
||||||
|
# Accept direct URL
|
||||||
|
if (u = obj["url"]?.try &.as_s?) && !u.empty?
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
# Accept ciphered URL that convert_url can handle
|
||||||
|
if (sc = obj["signatureCipher"]?.try &.as_s?) && !sc.empty?
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
if (c = obj["cipher"]?.try &.as_s?) && !c.empty?
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
# Mutates streaming_data by patching only the sections missing a usable URL
|
||||||
|
# Returns flags indicating what was patched.
|
||||||
|
def patch_streaming_data_if_missing!(streaming_data : Hash(String, JSON::Any), fallback_sd : Hash(String, JSON::Any)) : NamedTuple(patched_formats: Bool, patched_adaptive: Bool)
|
||||||
|
patched_formats = false
|
||||||
|
patched_adaptive = false
|
||||||
|
|
||||||
|
# Adaptive formats
|
||||||
|
unless has_usable_stream?(streaming_data["adaptiveFormats"]?)
|
||||||
|
if has_usable_stream?(fallback_sd["adaptiveFormats"]?)
|
||||||
|
streaming_data["adaptiveFormats"] = fallback_sd["adaptiveFormats"]
|
||||||
|
patched_adaptive = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Progressive formats
|
||||||
|
unless has_usable_stream?(streaming_data["formats"]?)
|
||||||
|
if has_usable_stream?(fallback_sd["formats"]?)
|
||||||
|
streaming_data["formats"] = fallback_sd["formats"]
|
||||||
|
patched_formats = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
{patched_formats: patched_formats, patched_adaptive: patched_adaptive}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def extract_video_info(video_id : String)
|
def extract_video_info(video_id : String)
|
||||||
# Init client config for the API
|
# Init client config for the API
|
||||||
client_config = YoutubeAPI::ClientConfig.new
|
client_config = YoutubeAPI::ClientConfig.new
|
||||||
@ -109,8 +162,14 @@ def extract_video_info(video_id : String)
|
|||||||
params["reason"] = JSON::Any.new(reason) if reason
|
params["reason"] = JSON::Any.new(reason) if reason
|
||||||
|
|
||||||
if !CONFIG.invidious_companion.present?
|
if !CONFIG.invidious_companion.present?
|
||||||
if player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil?
|
# Fix for issue #5420: /api/v1/videos/<video_id> endpoint has formatStreams blank
|
||||||
LOGGER.warn("Missing URLs for adaptive formats, falling back to other YT clients.")
|
# Determine if we are missing URLs for either adaptive or progressive formats
|
||||||
|
need_adaptive = player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil?
|
||||||
|
need_formats = player_response.dig?("streamingData", "formats", 0, "url").nil?
|
||||||
|
|
||||||
|
if need_adaptive || need_formats
|
||||||
|
missing = [need_adaptive ? "adaptiveFormats" : nil, need_formats ? "formats" : nil].compact.join(", ")
|
||||||
|
LOGGER.warn("Missing URLs for #{missing}, falling back to other YT clients.")
|
||||||
players_fallback = {YoutubeAPI::ClientType::TvHtml5, YoutubeAPI::ClientType::WebMobile}
|
players_fallback = {YoutubeAPI::ClientType::TvHtml5, YoutubeAPI::ClientType::WebMobile}
|
||||||
|
|
||||||
players_fallback.each do |player_fallback|
|
players_fallback.each do |player_fallback|
|
||||||
@ -118,9 +177,13 @@ def extract_video_info(video_id : String)
|
|||||||
|
|
||||||
next if !(player_fallback_response = try_fetch_streaming_data(video_id, client_config))
|
next if !(player_fallback_response = try_fetch_streaming_data(video_id, client_config))
|
||||||
|
|
||||||
if player_fallback_response.dig?("streamingData", "adaptiveFormats", 0, "url")
|
# Only patch what is actually missing to avoid downgrading good data
|
||||||
streaming_data = player_response["streamingData"].as_h
|
streaming_data = player_response["streamingData"].as_h
|
||||||
streaming_data["adaptiveFormats"] = player_fallback_response["streamingData"]["adaptiveFormats"]
|
fallback_sd = player_fallback_response["streamingData"].as_h
|
||||||
|
patched = Invidious::Videos::ParserHelpers.patch_streaming_data_if_missing!(streaming_data, fallback_sd)
|
||||||
|
|
||||||
|
if patched[:patched_adaptive] || patched[:patched_formats]
|
||||||
|
LOGGER.debug("fallback_patched: client=#{player_fallback} video=#{video_id} patched_adaptive=#{patched[:patched_adaptive]} patched_formats=#{patched[:patched_formats]}")
|
||||||
player_response["streamingData"] = JSON::Any.new(streaming_data)
|
player_response["streamingData"] = JSON::Any.new(streaming_data)
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
Loading…
x
Reference in New Issue
Block a user