Add support for caching IOS client player requests

When a client requests HLS streams, Invidious will first check the
database to see if the cached video has any HLS streams. If not
we request the IOS client and update the streamingData field with
the now gotten HLS manifest data.

Afterwards, we update the cached video in the database.
This commit is contained in:
syeopite 2024-11-10 23:19:11 -08:00
parent 122c8598ba
commit ffc44b0cff
No known key found for this signature in database
GPG Key ID: A73C186DA3955A1A
3 changed files with 49 additions and 22 deletions

View File

@ -52,7 +52,7 @@ module Invidious::Routes::Watch
env.params.query.delete_all("listen")
begin
video = get_video(id, region: params.region, force_hls: (params.quality == "hls"))
video = get_video(id, region: params.region, get_hls: (params.quality == "hls"))
rescue ex : NotFoundException
LOGGER.error("get_video not found: #{id} : #{ex.message}")
return error_template(404, ex)

View File

@ -294,8 +294,8 @@ struct Video
predicate_bool upcoming, isUpcoming
end
def get_video(id, refresh = true, region = nil, force_hls = false, force_refresh = false)
if (video = Invidious::Database::Videos.select(id)) && !region && !force_hls
def get_video(id, refresh = true, region = nil, get_hls = false, force_refresh = false)
if (video = Invidious::Database::Videos.select(id)) && !region
# If record was last updated over 10 minutes ago, or video has since premiered,
# refresh (expire param in response lasts for 6 hours)
if (refresh &&
@ -312,8 +312,21 @@ def get_video(id, refresh = true, region = nil, force_hls = false, force_refresh
end
end
else
video = fetch_video(id, region, force_hls)
Invidious::Database::Videos.insert(video) if !region && !force_hls
video = fetch_video(id, region)
Invidious::Database::Videos.insert(video) if !region
end
# The video object we got above could be from a previous request that was not
# done through the IOS client. If the users wants HLS we should check if
# a manifest exists in the data returned. If not we will rerequest one.
if get_hls && !video.hls_manifest_url
begin
video_with_hls_data = update_video_object_with_hls_data(id, video)
return video if !video_with_hls_data
Invidious::Database::Videos.update(video_with_hls_data) if !region
rescue ex
# Use old database video if IOS client request fails
end
end
return video
@ -323,8 +336,8 @@ rescue DB::Error
return fetch_video(id, region)
end
def fetch_video(id, region, force_hls = false)
info = extract_video_info(video_id: id, force_hls: force_hls)
def fetch_video(id, region)
info = extract_video_info(video_id: id)
if reason = info["reason"]?
if reason == "Video unavailable"

View File

@ -50,7 +50,7 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
}
end
def extract_video_info(video_id : String, force_hls : Bool = false)
def extract_video_info(video_id : String)
# Init client config for the API
client_config = YoutubeAPI::ClientConfig.new
@ -101,10 +101,7 @@ def extract_video_info(video_id : String, force_hls : Bool = false)
params["reason"] = JSON::Any.new(reason) if reason
new_player_response = nil
if force_hls
client_config.client_type = YoutubeAPI::ClientType::IOS
new_player_response = try_fetch_streaming_data(video_id, client_config)
else
# 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?
@ -124,7 +121,6 @@ def extract_video_info(video_id : String, force_hls : Bool = false)
new_player_response = try_fetch_streaming_data(video_id, client_config)
end
end
end
# Replace player response and reset reason
if !new_player_response.nil?
@ -157,6 +153,24 @@ def extract_video_info(video_id : String, force_hls : Bool = false)
return params
end
def update_video_object_with_hls_data(id : String, video : Video)
client_config = YoutubeAPI::ClientConfig.new(client_type: YoutubeAPI::ClientType::IOS)
new_player_response = try_fetch_streaming_data(id, client_config)
current_streaming_data = video.info["streamingData"].try &.as_h
return nil if !new_player_response
if current_streaming_data && (manifest = new_player_response.dig?("streamingData", "hlsManifestUrl"))
current_streaming_data["hlsManifestUrl"] = JSON::Any.new(manifest.as_s)
video.info["streamingData"] = JSON::Any.new(current_streaming_data)
return video
end
return nil
end
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.")
response = YoutubeAPI.player(video_id: id, params: "2AMB", client_config: client_config)