2018-08-05 04:07:38 +00:00
|
|
|
class SearchVideo
|
|
|
|
add_mapping({
|
|
|
|
title: String,
|
|
|
|
id: String,
|
|
|
|
author: String,
|
|
|
|
ucid: String,
|
|
|
|
published: Time,
|
2018-08-10 14:44:19 +00:00
|
|
|
views: Int64,
|
2018-08-05 04:07:38 +00:00
|
|
|
description: String,
|
|
|
|
description_html: String,
|
|
|
|
length_seconds: Int32,
|
2018-09-20 14:36:09 +00:00
|
|
|
live_now: Bool,
|
2018-10-16 16:15:14 +00:00
|
|
|
paid: Bool,
|
|
|
|
premium: Bool,
|
2018-08-05 04:07:38 +00:00
|
|
|
})
|
|
|
|
end
|
|
|
|
|
2018-09-20 14:36:09 +00:00
|
|
|
class SearchPlaylistVideo
|
|
|
|
add_mapping({
|
|
|
|
title: String,
|
|
|
|
id: String,
|
|
|
|
length_seconds: Int32,
|
|
|
|
})
|
|
|
|
end
|
|
|
|
|
|
|
|
class SearchPlaylist
|
|
|
|
add_mapping({
|
|
|
|
title: String,
|
|
|
|
id: String,
|
|
|
|
author: String,
|
|
|
|
ucid: String,
|
|
|
|
video_count: Int32,
|
|
|
|
videos: Array(SearchPlaylistVideo),
|
|
|
|
})
|
|
|
|
end
|
|
|
|
|
|
|
|
class SearchChannel
|
|
|
|
add_mapping({
|
|
|
|
author: String,
|
|
|
|
ucid: String,
|
|
|
|
author_thumbnail: String,
|
|
|
|
subscriber_count: Int32,
|
|
|
|
video_count: Int32,
|
|
|
|
description: String,
|
|
|
|
description_html: String,
|
|
|
|
})
|
|
|
|
end
|
|
|
|
|
|
|
|
alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist
|
|
|
|
|
2018-09-13 22:47:31 +00:00
|
|
|
def channel_search(query, page, channel)
|
|
|
|
client = make_client(YT_URL)
|
|
|
|
|
|
|
|
response = client.get("/user/#{channel}")
|
|
|
|
document = XML.parse_html(response.body)
|
|
|
|
canonical = document.xpath_node(%q(//link[@rel="canonical"]))
|
|
|
|
|
|
|
|
if !canonical
|
|
|
|
response = client.get("/channel/#{channel}")
|
|
|
|
document = XML.parse_html(response.body)
|
|
|
|
canonical = document.xpath_node(%q(//link[@rel="canonical"]))
|
|
|
|
end
|
|
|
|
|
|
|
|
if !canonical
|
2018-09-20 14:36:09 +00:00
|
|
|
return 0, [] of SearchItem
|
2018-09-13 22:47:31 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
ucid = canonical["href"].split("/")[-1]
|
|
|
|
|
|
|
|
url = produce_channel_search_url(ucid, query, page)
|
|
|
|
response = client.get(url)
|
|
|
|
json = JSON.parse(response.body)
|
|
|
|
|
|
|
|
if json["content_html"]? && !json["content_html"].as_s.empty?
|
|
|
|
document = XML.parse_html(json["content_html"].as_s)
|
|
|
|
nodeset = document.xpath_nodes(%q(//li[contains(@class, "feed-item-container")]))
|
|
|
|
|
|
|
|
count = nodeset.size
|
2018-09-20 14:36:09 +00:00
|
|
|
items = extract_items(nodeset)
|
2018-09-13 22:47:31 +00:00
|
|
|
else
|
|
|
|
count = 0
|
2018-09-20 14:36:09 +00:00
|
|
|
items = [] of SearchItem
|
2018-09-13 22:47:31 +00:00
|
|
|
end
|
|
|
|
|
2018-09-20 14:36:09 +00:00
|
|
|
return count, items
|
2018-09-13 22:47:31 +00:00
|
|
|
end
|
|
|
|
|
2018-09-20 14:36:09 +00:00
|
|
|
def search(query, page = 1, search_params = produce_search_params(content_type: "all"))
|
2018-08-04 20:30:44 +00:00
|
|
|
client = make_client(YT_URL)
|
2018-08-27 20:23:25 +00:00
|
|
|
if query.empty?
|
2018-09-20 14:36:09 +00:00
|
|
|
return {0, [] of SearchItem}
|
2018-08-27 20:23:25 +00:00
|
|
|
end
|
|
|
|
|
2018-09-25 22:55:32 +00:00
|
|
|
html = client.get("/results?q=#{URI.escape(query)}&page=#{page}&sp=#{search_params}&hl=en&disable_polymer=1").body
|
2018-08-05 04:07:38 +00:00
|
|
|
if html.empty?
|
2018-09-20 14:36:09 +00:00
|
|
|
return {0, [] of SearchItem}
|
2018-08-05 04:07:38 +00:00
|
|
|
end
|
|
|
|
|
2018-08-04 20:30:44 +00:00
|
|
|
html = XML.parse_html(html)
|
2018-08-10 14:44:19 +00:00
|
|
|
nodeset = html.xpath_nodes(%q(//ol[@class="item-section"]/li))
|
2018-09-20 14:36:09 +00:00
|
|
|
items = extract_items(nodeset)
|
2018-08-04 20:30:44 +00:00
|
|
|
|
2018-09-20 14:36:09 +00:00
|
|
|
return {nodeset.size, items}
|
2018-08-04 20:30:44 +00:00
|
|
|
end
|
2018-08-04 22:12:58 +00:00
|
|
|
|
2018-09-17 21:38:18 +00:00
|
|
|
def produce_search_params(sort : String = "relevance", date : String = "", content_type : String = "",
|
|
|
|
duration : String = "", features : Array(String) = [] of String)
|
2018-08-04 22:12:58 +00:00
|
|
|
head = "\x08"
|
2018-08-27 20:23:25 +00:00
|
|
|
head += case sort
|
2018-08-04 22:12:58 +00:00
|
|
|
when "relevance"
|
|
|
|
"\x00"
|
|
|
|
when "rating"
|
|
|
|
"\x01"
|
2018-08-30 22:42:30 +00:00
|
|
|
when "upload_date", "date"
|
2018-08-04 22:12:58 +00:00
|
|
|
"\x02"
|
2018-08-30 22:42:30 +00:00
|
|
|
when "view_count", "views"
|
2018-08-04 22:12:58 +00:00
|
|
|
"\x03"
|
|
|
|
else
|
2018-08-27 20:23:25 +00:00
|
|
|
raise "No sort #{sort}"
|
2018-08-04 22:12:58 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
body = ""
|
|
|
|
body += case date
|
|
|
|
when "hour"
|
|
|
|
"\x08\x01"
|
|
|
|
when "today"
|
|
|
|
"\x08\x02"
|
|
|
|
when "week"
|
|
|
|
"\x08\x03"
|
|
|
|
when "month"
|
|
|
|
"\x08\x04"
|
|
|
|
when "year"
|
|
|
|
"\x08\x05"
|
|
|
|
else
|
|
|
|
""
|
|
|
|
end
|
|
|
|
|
|
|
|
body += case content_type
|
|
|
|
when "video"
|
|
|
|
"\x10\x01"
|
|
|
|
when "channel"
|
|
|
|
"\x10\x02"
|
|
|
|
when "playlist"
|
|
|
|
"\x10\x03"
|
|
|
|
when "movie"
|
|
|
|
"\x10\x04"
|
|
|
|
when "show"
|
|
|
|
"\x10\x05"
|
2018-09-20 14:36:09 +00:00
|
|
|
when "all"
|
2018-08-04 22:12:58 +00:00
|
|
|
""
|
2018-09-20 14:36:09 +00:00
|
|
|
else
|
|
|
|
"\x10\x01"
|
2018-08-04 22:12:58 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
body += case duration
|
|
|
|
when "short"
|
|
|
|
"\x18\x01"
|
|
|
|
when "long"
|
|
|
|
"\x18\x02"
|
|
|
|
else
|
|
|
|
""
|
|
|
|
end
|
|
|
|
|
|
|
|
features.each do |feature|
|
|
|
|
body += case feature
|
|
|
|
when "hd"
|
|
|
|
"\x20\x01"
|
|
|
|
when "subtitles"
|
|
|
|
"\x28\x01"
|
2018-08-30 22:42:30 +00:00
|
|
|
when "creative_commons", "cc"
|
2018-08-04 22:12:58 +00:00
|
|
|
"\x30\x01"
|
|
|
|
when "3d"
|
|
|
|
"\x38\x01"
|
2018-09-20 15:16:10 +00:00
|
|
|
when "live", "livestream"
|
2018-08-04 22:12:58 +00:00
|
|
|
"\x40\x01"
|
|
|
|
when "purchased"
|
|
|
|
"\x48\x01"
|
|
|
|
when "4k"
|
|
|
|
"\x70\x01"
|
|
|
|
when "360"
|
|
|
|
"\x78\x01"
|
|
|
|
when "location"
|
|
|
|
"\xb8\x01\x01"
|
|
|
|
when "hdr"
|
|
|
|
"\xc8\x01\x01"
|
|
|
|
else
|
|
|
|
raise "Unknown feature #{feature}"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if body.size > 0
|
2018-09-17 21:38:18 +00:00
|
|
|
token = head + "\x12" + body.size.unsafe_chr + body
|
2018-08-04 22:12:58 +00:00
|
|
|
else
|
|
|
|
token = head
|
|
|
|
end
|
|
|
|
|
|
|
|
token = Base64.urlsafe_encode(token)
|
|
|
|
token = URI.escape(token)
|
|
|
|
|
|
|
|
return token
|
|
|
|
end
|
2018-09-13 22:47:31 +00:00
|
|
|
|
|
|
|
def produce_channel_search_url(ucid, query, page)
|
|
|
|
page = "#{page}"
|
|
|
|
|
2018-09-17 21:38:18 +00:00
|
|
|
meta = "\x12\x06search"
|
|
|
|
meta += "\x30\x02"
|
|
|
|
meta += "\x38\x01"
|
|
|
|
meta += "\x60\x01"
|
|
|
|
meta += "\x6a\x00"
|
2018-09-13 22:47:31 +00:00
|
|
|
meta += "\xb8\x01\x00"
|
2018-09-17 21:38:18 +00:00
|
|
|
meta += "\x7a"
|
|
|
|
meta += page.size.unsafe_chr
|
|
|
|
meta += page
|
2018-09-13 22:47:31 +00:00
|
|
|
|
|
|
|
meta = Base64.urlsafe_encode(meta)
|
|
|
|
meta = URI.escape(meta)
|
|
|
|
|
|
|
|
continuation = "\x12"
|
2018-09-17 21:38:18 +00:00
|
|
|
continuation += ucid.size.unsafe_chr
|
2018-09-13 22:47:31 +00:00
|
|
|
continuation += ucid
|
|
|
|
continuation += "\x1a"
|
2018-09-17 21:38:18 +00:00
|
|
|
continuation += meta.size.unsafe_chr
|
2018-09-13 22:47:31 +00:00
|
|
|
continuation += meta
|
|
|
|
continuation += "\x5a"
|
2018-09-17 21:38:18 +00:00
|
|
|
continuation += query.size.unsafe_chr
|
2018-09-13 22:47:31 +00:00
|
|
|
continuation += query
|
|
|
|
|
2018-09-17 21:38:18 +00:00
|
|
|
continuation = continuation.size.unsafe_chr + continuation
|
2018-09-13 22:47:31 +00:00
|
|
|
continuation = "\xe2\xa9\x85\xb2\x02" + continuation
|
|
|
|
|
|
|
|
continuation = Base64.urlsafe_encode(continuation)
|
|
|
|
continuation = URI.escape(continuation)
|
|
|
|
|
|
|
|
url = "/browse_ajax?continuation=#{continuation}"
|
|
|
|
|
|
|
|
return url
|
|
|
|
end
|