Merge pull request #1929 from Svallinn/channel_playlist_fix

Fix channels' playlists fetching
This commit is contained in:
TheFrenchGhosty 2021-04-03 21:21:42 +00:00 committed by GitHub
commit c0997f951c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 30 additions and 45 deletions

View file

@ -41,12 +41,6 @@ describe "Helper" do
end end
end end
describe "#extract_channel_playlists_cursor" do
it "correctly extracts a playlists cursor from the given URL" do
extract_channel_playlists_cursor("4qmFsgLRARIYVUNDajk1NklGNjJGYlQ3R291c3phajl3GrQBRWdsd2JHRjViR2x6ZEhNWUF5QUJNQUk0QVdBQmFnQjZabEZWYkZCaE1XczFVbFpHZDJGV09XNWxWelI0V0RGR2VWSnVWbUZOV0Vwc1ZHcG5lRmd3TVU1aVZXdDRWMWN4YzFGdFNuTmtlbWh4VGpCd1NWTllVa1pTYTJNeFlVUmtlRmt3Y0ZWVWJWRXdWbnBzTkU1V1JqRmhNVGxFVm14dmQwMXFhRzVXZDdnQkFBJTNEJTNE", false).should eq("AIOkY9EQpi_gyn1_QrFuZ1reN81_MMmI1YmlBblw8j7JHItEFG5h7qcJTNd4W9x5Quk_CVZ028gW")
end
end
describe "#produce_playlist_continuation" do describe "#produce_playlist_continuation" do
it "correctly produces ctoken for requesting index `x` of a playlist" do it "correctly produces ctoken for requesting index `x` of a playlist" do
produce_playlist_continuation("UUCla9fZca4I7KagBtgRGnOw", 100).should eq("4qmFsgJNEhpWTFVVQ2xhOWZaY2E0STdLYWdCdGdSR25PdxoUQ0FGNkJsQlVPa05IVVElM0QlM0SaAhhVVUNsYTlmWmNhNEk3S2FnQnRnUkduT3c%3D") produce_playlist_continuation("UUCla9fZca4I7KagBtgRGnOw", 100).should eq("4qmFsgJNEhpWTFVVQ2xhOWZaY2E0STdLYWdCdGdSR25PdxoUQ0FGNkJsQlVPa05IVVElM0QlM0SaAhhVVUNsYTlmWmNhNEk3S2FnQnRnUkduT3c%3D")

View file

@ -1699,7 +1699,7 @@ get "/channel/:ucid" do |env|
sort_options = {"last", "oldest", "newest"} sort_options = {"last", "oldest", "newest"}
sort_by ||= "last" sort_by ||= "last"
items, continuation = fetch_channel_playlists(channel.ucid, channel.author, channel.auto_generated, continuation, sort_by) items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by)
items.uniq! do |item| items.uniq! do |item|
if item.responds_to?(:title) if item.responds_to?(:title)
item.title item.title
@ -1766,7 +1766,7 @@ get "/channel/:ucid/playlists" do |env|
next env.redirect "/channel/#{channel.ucid}" next env.redirect "/channel/#{channel.ucid}"
end end
items, continuation = fetch_channel_playlists(channel.ucid, channel.author, channel.auto_generated, continuation, sort_by) items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by)
items = items.select { |item| item.is_a?(SearchPlaylist) }.map { |item| item.as(SearchPlaylist) } items = items.select { |item| item.is_a?(SearchPlaylist) }.map { |item| item.as(SearchPlaylist) }
items.each { |item| item.author = "" } items.each { |item| item.author = "" }
@ -2467,7 +2467,7 @@ end
next error_json(500, ex) next error_json(500, ex)
end end
items, continuation = fetch_channel_playlists(channel.ucid, channel.author, channel.auto_generated, continuation, sort_by) items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by)
JSON.build do |json| JSON.build do |json|
json.object do json.object do

View file

@ -355,14 +355,22 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
return channel return channel
end end
def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by) def fetch_channel_playlists(ucid, author, continuation, sort_by)
if continuation || auto_generated if continuation
url = produce_channel_playlists_url(ucid, continuation, sort_by, auto_generated) response_json = request_youtube_api_browse(continuation)
result = JSON.parse(response_json)
continuationItems = result["onResponseReceivedActions"]?
.try &.[0]["appendContinuationItemsAction"]["continuationItems"]
response = YT_POOL.client &.get(url) return [] of SearchItem, nil if !continuationItems
continuation = response.body.match(/"continuation":"(?<continuation>[^"]+)"/).try &.["continuation"]? items = [] of SearchItem
initial_data = JSON.parse(response.body).as_a.find(&.["response"]?).try &.as_h continuationItems.as_a.select(&.as_h.has_key?("gridPlaylistRenderer")).each { |item|
extract_item(item, author, ucid).try { |t| items << t }
}
continuation = continuationItems.as_a.last["continuationItemRenderer"]?
.try &.["continuationEndpoint"]["continuationCommand"]["token"].as_s
else else
url = "/channel/#{ucid}/playlists?flow=list&view=1" url = "/channel/#{ucid}/playlists?flow=list&view=1"
@ -377,13 +385,12 @@ def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by)
end end
response = YT_POOL.client &.get(url) response = YT_POOL.client &.get(url)
continuation = response.body.match(/"continuation":"(?<continuation>[^"]+)"/).try &.["continuation"]?
initial_data = extract_initial_data(response.body) initial_data = extract_initial_data(response.body)
end
return [] of SearchItem, nil if !initial_data return [] of SearchItem, nil if !initial_data
items = extract_items(initial_data)
continuation = extract_channel_playlists_cursor(continuation, auto_generated) if continuation items = extract_items(initial_data, author, ucid)
continuation = response.body.match(/"token":"(?<continuation>[^"]+)"/).try &.["continuation"]?
end
return items, continuation return items, continuation
end end
@ -453,6 +460,15 @@ def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = "
return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en" return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"
end end
# ## NOTE: DEPRECATED
# Reason -> Unstable
# The Protobuf object must be provided with an id of the last playlist from the current "page"
# in order to fetch the next one accurately
# (if the id isn't included, entries shift around erratically between pages,
# leading to repetitions and skip overs)
#
# Since it's impossible to produce the appropriate Protobuf without an id being provided by the user,
# it's better to stick to continuation tokens provided by the first request and onward
def produce_channel_playlists_url(ucid, cursor, sort = "newest", auto_generated = false) def produce_channel_playlists_url(ucid, cursor, sort = "newest", auto_generated = false)
object = { object = {
"80226972:embedded" => { "80226972:embedded" => {
@ -499,31 +515,6 @@ def produce_channel_playlists_url(ucid, cursor, sort = "newest", auto_generated
return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en" return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"
end end
def extract_channel_playlists_cursor(cursor, auto_generated)
cursor = URI.decode_www_form(cursor)
.try { |i| Base64.decode(i) }
.try { |i| IO::Memory.new(i) }
.try { |i| Protodec::Any.parse(i) }
.try { |i| i["80226972:0:embedded"]["3:1:base64"].as_h.find { |k, v| k.starts_with? "15:" } }
.try &.[1]
if cursor.try &.as_h?
cursor = cursor.try { |i| Protodec::Any.cast_json(i.as_h) }
.try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) } || ""
else
cursor = cursor.try &.as_s || ""
end
if !auto_generated
cursor = URI.decode_www_form(cursor)
.try { |i| Base64.decode_string(i) }
end
return cursor
end
# TODO: Add "sort_by" # TODO: Add "sort_by"
def fetch_channel_community(ucid, continuation, locale, format, thin_mode) def fetch_channel_community(ucid, continuation, locale, format, thin_mode)
response = YT_POOL.client &.get("/channel/#{ucid}/community?gl=US&hl=en") response = YT_POOL.client &.get("/channel/#{ucid}/community?gl=US&hl=en")