Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve youtube api calls #1985

Merged
merged 5 commits into from
Jun 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 9 additions & 31 deletions src/invidious/channels.cr
Original file line number Diff line number Diff line change
Expand Up @@ -229,22 +229,8 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
page = 1

LOGGER.trace("fetch_channel: #{ucid} : Downloading channel videos page")
response_body = get_channel_videos_response(ucid, page, auto_generated: auto_generated)

videos = [] of SearchVideo
begin
initial_data = JSON.parse(response_body)
raise InfoException.new("Could not extract channel JSON") if !initial_data

LOGGER.trace("fetch_channel: #{ucid} : Extracting videos from channel videos page initial_data")
videos = extract_videos(initial_data.as_h, author, ucid)
rescue ex
if response_body.includes?("To continue with your YouTube experience, please fill out the form below.") ||
response_body.includes?("https://www.google.com/sorry/index")
raise InfoException.new("Could not extract channel info. Instance is likely blocked.")
end
raise ex
end
initial_data = get_channel_videos_response(ucid, page, auto_generated: auto_generated)
videos = extract_videos(initial_data, author, ucid)

LOGGER.trace("fetch_channel: #{ucid} : Extracting videos from channel RSS feed")
rss.xpath_nodes("//feed/entry").each do |entry|
Expand Down Expand Up @@ -304,10 +290,8 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
ids = [] of String

loop do
response_body = get_channel_videos_response(ucid, page, auto_generated: auto_generated)
initial_data = JSON.parse(response_body)
raise InfoException.new("Could not extract channel JSON") if !initial_data
videos = extract_videos(initial_data.as_h, author, ucid)
initial_data = get_channel_videos_response(ucid, page, auto_generated: auto_generated)
videos = extract_videos(initial_data, author, ucid)

count = videos.size
videos = videos.map { |video| ChannelVideo.new({
Expand Down Expand Up @@ -358,8 +342,7 @@ end
def fetch_channel_playlists(ucid, author, continuation, sort_by)
if continuation
response_json = request_youtube_api_browse(continuation)
result = JSON.parse(response_json)
continuationItems = result["onResponseReceivedActions"]?
continuationItems = response_json["onResponseReceivedActions"]?
.try &.[0]["appendContinuationItemsAction"]["continuationItems"]

return [] of SearchItem, nil if !continuationItems
Expand Down Expand Up @@ -964,21 +947,16 @@ def get_60_videos(ucid, author, page, auto_generated, sort_by = "newest")
videos = [] of SearchVideo

2.times do |i|
response_json = get_channel_videos_response(ucid, page * 2 + (i - 1), auto_generated: auto_generated, sort_by: sort_by)
initial_data = JSON.parse(response_json)
break if !initial_data
videos.concat extract_videos(initial_data.as_h, author, ucid)
initial_data = get_channel_videos_response(ucid, page * 2 + (i - 1), auto_generated: auto_generated, sort_by: sort_by)
videos.concat extract_videos(initial_data, author, ucid)
end

return videos.size, videos
end

def get_latest_videos(ucid)
response_json = get_channel_videos_response(ucid)
initial_data = JSON.parse(response_json)
return [] of SearchVideo if !initial_data
initial_data = get_channel_videos_response(ucid)
author = initial_data["metadata"]?.try &.["channelMetadataRenderer"]?.try &.["title"]?.try &.as_s
items = extract_videos(initial_data.as_h, author, ucid)

return items
return extract_videos(initial_data, author, ucid)
end
118 changes: 103 additions & 15 deletions src/invidious/helpers/youtube_api.cr
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,116 @@

# Hard-coded constants required by the API
HARDCODED_API_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
HARDCODED_CLIENT_VERS = "2.20210318.08.00"
HARDCODED_CLIENT_VERS = "2.20210330.08.00"

def request_youtube_api_browse(continuation)
####################################################################
# make_youtube_api_context(region)
#
# Return, as a Hash, the "context" data required to request the
# youtube API endpoints.
#
def make_youtube_api_context(region : String | Nil) : Hash
return {
"client" => {
"hl" => "en",
"gl" => region || "US", # Can't be empty!
"clientName" => "WEB",
"clientVersion" => HARDCODED_CLIENT_VERS,
},
}
end

####################################################################
# request_youtube_api_browse(continuation)
# request_youtube_api_browse(browse_id, params)
#
# Requests the youtubei/v1/browse endpoint with the required headers
# and POST data in order to get a JSON reply in english US that can
# be easily parsed.
#
# The requested data can either be:
#
# - A continuation token (ctoken). Depending on this token's
# contents, the returned data can be comments, playlist videos,
# search results, channel community tab, ...
#
# - A playlist ID (parameters MUST be an empty string)
#
def request_youtube_api_browse(continuation : String)
# JSON Request data, required by the API
data = {
"context": {
"client": {
"hl": "en",
"gl": "US",
"clientName": "WEB",
"clientVersion": HARDCODED_CLIENT_VERS,
},
},
"continuation": continuation,
"context" => make_youtube_api_context("US"),
"continuation" => continuation,
}

return _youtube_api_post_json("/youtubei/v1/browse", data)
end

def request_youtube_api_browse(browse_id : String, params : String)
# JSON Request data, required by the API
data = {
"browseId" => browse_id,
"context" => make_youtube_api_context("US"),
}

# Send the POST request and return result
# Append the additionnal parameters if those were provided
# (this is required for channel info, playlist and community, e.g)
if params != ""
data["params"] = params
end

return _youtube_api_post_json("/youtubei/v1/browse", data)
end

####################################################################
# request_youtube_api_search(search_query, params, region)
#
# Requests the youtubei/v1/search endpoint with the required headers
# and POST data in order to get a JSON reply. As the search results
# vary depending on the region, a region code can be specified in
# order to get non-US results.
#
# The requested data is a search string, with some additional
# paramters, formatted as a base64 string.
#
def request_youtube_api_search(search_query : String, params : String, region = nil)
# JSON Request data, required by the API
data = {
"query" => URI.encode_www_form(search_query),
"context" => make_youtube_api_context(region),
"params" => params,
}

return _youtube_api_post_json("/youtubei/v1/search", data)
end

####################################################################
# _youtube_api_post_json(endpoint, data)
#
# Internal function that does the actual request to youtube servers
# and handles errors.
#
# The requested data is an endpoint (URL without the domain part)
# and the data as a Hash object.
#
def _youtube_api_post_json(endpoint, data)
# Send the POST request and parse result
response = YT_POOL.client &.post(
"/youtubei/v1/browse?key=#{HARDCODED_API_KEY}",
headers: HTTP::Headers{"content-type" => "application/json"},
"#{endpoint}?key=#{HARDCODED_API_KEY}",
headers: HTTP::Headers{"content-type" => "application/json; charset=UTF-8"},
body: data.to_json
)

return response.body
initial_data = JSON.parse(response.body).as_h

# Error handling
if initial_data.has_key?("error")
code = initial_data["error"]["code"]
message = initial_data["error"]["message"].to_s.sub(/(\\n)+\^$/, "")

raise InfoException.new("Could not extract JSON. Youtube API returned \
error #{code} with message:<br>\"#{message}\"")
end

return initial_data
end
22 changes: 4 additions & 18 deletions src/invidious/playlists.cr
Original file line number Diff line number Diff line change
Expand Up @@ -361,16 +361,7 @@ def fetch_playlist(plid, locale)
plid = "UU#{plid.lchop("UC")}"
end

response = YT_POOL.client &.get("/playlist?list=#{plid}&hl=en")
if response.status_code != 200
if response.headers["location"]?.try &.includes? "/sorry/index"
raise InfoException.new("Could not extract playlist info. Instance is likely blocked.")
else
raise InfoException.new("Not a playlist.")
end
end

initial_data = extract_initial_data(response.body)
initial_data = request_youtube_api_browse("VL" + plid, params: "")

playlist_sidebar_renderer = initial_data["sidebar"]?.try &.["playlistSidebarRenderer"]?.try &.["items"]?
raise InfoException.new("Could not extract playlistSidebarRenderer.") if !playlist_sidebar_renderer
Expand Down Expand Up @@ -451,17 +442,12 @@ def get_playlist_videos(db, playlist, offset, locale = nil, continuation = nil)
offset = (offset / 100).to_i64 * 100_i64

ctoken = produce_playlist_continuation(playlist.id, offset)
initial_data = JSON.parse(request_youtube_api_browse(ctoken)).as_h
initial_data = request_youtube_api_browse(ctoken)
else
response = YT_POOL.client &.get("/playlist?list=#{playlist.id}&gl=US&hl=en")
initial_data = extract_initial_data(response.body)
initial_data = request_youtube_api_browse("VL" + playlist.id, params: "")
end

if initial_data
return extract_playlist_videos(initial_data)
else
return [] of PlaylistVideo
end
return extract_playlist_videos(initial_data)
end
end

Expand Down
10 changes: 2 additions & 8 deletions src/invidious/search.cr
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,7 @@ def channel_search(query, page, channel)
continuation = produce_channel_search_continuation(ucid, query, page)
response_json = request_youtube_api_browse(continuation)

result = JSON.parse(response_json)
continuationItems = result["onResponseReceivedActions"]?
continuationItems = response_json["onResponseReceivedActions"]?
.try &.[0]["appendContinuationItemsAction"]["continuationItems"]

return 0, [] of SearchItem if !continuationItems
Expand All @@ -264,14 +263,9 @@ end
def search(query, search_params = produce_search_params(content_type: "all"), region = nil)
return 0, [] of SearchItem if query.empty?

body = YT_POOL.client(region, &.get("/results?search_query=#{URI.encode_www_form(query)}&sp=#{search_params}&hl=en").body)
return 0, [] of SearchItem if body.empty?

initial_data = extract_initial_data(body)
initial_data = request_youtube_api_search(query, search_params, region)
items = extract_items(initial_data)

# initial_data["estimatedResults"]?.try &.as_s.to_i64

return items.size, items
end

Expand Down