invidious-experimenting/src/invidious/search.cr

307 lines
7.8 KiB
Crystal
Raw Normal View History

2019-03-30 03:00:02 +05:30
struct SearchVideo
2019-06-07 23:09:12 +05:30
def to_xml(host_url, auto_generated, xml : XML::Builder)
xml.element("entry") do
xml.element("id") { xml.text "yt:video:#{self.id}" }
xml.element("yt:videoId") { xml.text self.id }
xml.element("yt:channelId") { xml.text self.ucid }
xml.element("title") { xml.text self.title }
xml.element("link", rel: "alternate", href: "#{host_url}/watch?v=#{self.id}")
xml.element("author") do
if auto_generated
xml.element("name") { xml.text self.author }
xml.element("uri") { xml.text "#{host_url}/channel/#{self.ucid}" }
else
xml.element("name") { xml.text author }
xml.element("uri") { xml.text "#{host_url}/channel/#{ucid}" }
end
end
xml.element("content", type: "xhtml") do
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
xml.element("a", href: "#{host_url}/watch?v=#{self.id}") do
xml.element("img", src: "#{host_url}/vi/#{self.id}/mqdefault.jpg")
end
end
end
xml.element("published") { xml.text self.published.to_s("%Y-%m-%dT%H:%M:%S%:z") }
xml.element("media:group") do
xml.element("media:title") { xml.text self.title }
xml.element("media:thumbnail", url: "#{host_url}/vi/#{self.id}/mqdefault.jpg",
width: "320", height: "180")
xml.element("media:description") { xml.text self.description }
end
xml.element("media:community") do
xml.element("media:statistics", views: self.views)
end
end
end
def to_xml(host_url, auto_generated, xml : XML::Builder | Nil = nil)
if xml
to_xml(host_url, auto_generated, xml)
else
XML.build do |json|
to_xml(host_url, auto_generated, xml)
end
end
end
db_mapping({
title: String,
id: String,
author: String,
ucid: String,
published: Time,
views: Int64,
description: String,
description_html: String,
length_seconds: Int32,
live_now: Bool,
paid: Bool,
premium: Bool,
premiere_timestamp: Time?,
2018-08-05 09:37:38 +05:30
})
end
2019-03-30 03:00:02 +05:30
struct SearchPlaylistVideo
db_mapping({
title: String,
id: String,
length_seconds: Int32,
})
end
2019-03-30 03:00:02 +05:30
struct SearchPlaylist
db_mapping({
2019-03-17 19:30:00 +05:30
title: String,
id: String,
author: String,
ucid: String,
video_count: Int32,
videos: Array(SearchPlaylistVideo),
thumbnail_id: String?,
})
end
2019-03-30 03:00:02 +05:30
struct SearchChannel
db_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-14 04:17:31 +05:30
def channel_search(query, page, channel)
client = make_client(YT_URL)
2019-04-12 00:22:09 +05:30
response = client.get("/channel/#{channel}?disable_polymer=1&hl=en&gl=US")
2018-09-14 04:17:31 +05:30
document = XML.parse_html(response.body)
canonical = document.xpath_node(%q(//link[@rel="canonical"]))
if !canonical
2019-04-12 00:22:09 +05:30
response = client.get("/c/#{channel}?disable_polymer=1&hl=en&gl=US")
2018-09-14 04:17:31 +05:30
document = XML.parse_html(response.body)
canonical = document.xpath_node(%q(//link[@rel="canonical"]))
end
if !canonical
response = client.get("/user/#{channel}?disable_polymer=1&hl=en&gl=US")
document = XML.parse_html(response.body)
canonical = document.xpath_node(%q(//link[@rel="canonical"]))
end
2018-09-14 04:17:31 +05:30
if !canonical
return 0, [] of SearchItem
2018-09-14 04:17:31 +05:30
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
items = extract_items(nodeset)
2018-09-14 04:17:31 +05:30
else
count = 0
items = [] of SearchItem
2018-09-14 04:17:31 +05:30
end
return count, items
2018-09-14 04:17:31 +05:30
end
2019-02-07 05:51:40 +05:30
def search(query, page = 1, search_params = produce_search_params(content_type: "all"), proxies = nil, region = nil)
client = make_client(YT_URL, proxies, region)
2018-08-28 01:53:25 +05:30
if query.empty?
return {0, [] of SearchItem}
2018-08-28 01:53:25 +05:30
end
2018-09-26 04:25:32 +05:30
html = client.get("/results?q=#{URI.escape(query)}&page=#{page}&sp=#{search_params}&hl=en&disable_polymer=1").body
2018-08-05 09:37:38 +05:30
if html.empty?
return {0, [] of SearchItem}
2018-08-05 09:37:38 +05:30
end
2018-08-05 02:00:44 +05:30
html = XML.parse_html(html)
nodeset = html.xpath_nodes(%q(//ol[@class="item-section"]/li))
items = extract_items(nodeset)
2018-08-05 02:00:44 +05:30
return {nodeset.size, items}
2018-08-05 02:00:44 +05:30
end
2018-09-18 03:08:18 +05:30
def produce_search_params(sort : String = "relevance", date : String = "", content_type : String = "",
duration : String = "", features : Array(String) = [] of String)
head = "\x08"
2018-08-28 01:53:25 +05:30
head += case sort
when "relevance"
"\x00"
when "rating"
"\x01"
2018-08-31 04:12:30 +05:30
when "upload_date", "date"
"\x02"
2018-08-31 04:12:30 +05:30
when "view_count", "views"
"\x03"
else
2018-08-28 01:53:25 +05:30
raise "No sort #{sort}"
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"
when "all"
""
else
"\x10\x01"
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-31 04:12:30 +05:30
when "creative_commons", "cc"
"\x30\x01"
when "3d"
"\x38\x01"
when "live", "livestream"
"\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
2019-02-27 02:01:37 +05:30
if !body.empty?
2018-09-18 03:08:18 +05:30
token = head + "\x12" + body.size.unsafe_chr + body
else
token = head
end
token = Base64.urlsafe_encode(token)
token = URI.escape(token)
return token
end
2018-09-14 04:17:31 +05:30
def produce_channel_search_url(ucid, query, page)
page = "#{page}"
meta = IO::Memory.new
meta.write(Bytes[0x12, 0x06])
meta.print("search")
meta.write(Bytes[0x30, 0x02])
meta.write(Bytes[0x38, 0x01])
meta.write(Bytes[0x60, 0x01])
meta.write(Bytes[0x6a, 0x00])
meta.write(Bytes[0xb8, 0x01, 0x00])
meta.write(Bytes[0x7a, page.size])
meta.print(page)
meta.rewind
meta = Base64.urlsafe_encode(meta.to_slice)
2018-09-14 04:17:31 +05:30
meta = URI.escape(meta)
continuation = IO::Memory.new
continuation.write(Bytes[0x12, ucid.size])
continuation.print(ucid)
continuation.write(Bytes[0x1a, meta.size])
continuation.print(meta)
continuation.write(Bytes[0x5a, query.size])
continuation.print(query)
continuation.rewind
continuation = continuation.gets_to_end
2018-09-14 04:17:31 +05:30
wrapper = IO::Memory.new
wrapper.write(Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02, continuation.size])
wrapper.print(continuation)
wrapper.rewind
2018-09-14 04:17:31 +05:30
wrapper = Base64.urlsafe_encode(wrapper.to_slice)
wrapper = URI.escape(wrapper)
2018-09-14 04:17:31 +05:30
url = "/browse_ajax?continuation=#{wrapper}&gl=US&hl=en"
2018-09-14 04:17:31 +05:30
return url
end