class SearchVideo
add_mapping({
title: String,
id: String,
author: String,
ucid: String,
published: Time,
view_count: Int64,
description: String,
description_html: String,
length_seconds: Int32,
})
end
def search(query, page = 1, search_params = build_search_params(content_type: "video"))
client = make_client(YT_URL)
html = client.get("/results?q=#{URI.escape(query)}&page=#{page}&sp=#{search_params}").body
if html.empty?
return [] of SearchVideo
end
html = XML.parse_html(html)
videos = [] of SearchVideo
html.xpath_nodes(%q(//ol[@class="item-section"]/li)).each do |node|
anchor = node.xpath_node(%q(.//h3[contains(@class,"yt-lockup-title")]/a))
if !anchor
next
end
if anchor["href"].starts_with? "https://www.googleadservices.com"
next
end
title = anchor.content.strip
video_id = anchor["href"].lchop("/watch?v=")
anchor = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-byline")]/a))
if !anchor
next
end
author = anchor.content
author_url = anchor["href"]
ucid = author_url.split("/")[-1]
metadata = node.xpath_nodes(%q(.//div[contains(@class,"yt-lockup-meta")]/ul/li))
if metadata.size == 0
next
elsif metadata.size == 1
# Skip movies
if metadata[0]["class"].includes? "ytd-movie-renderer"
next
end
view_count = metadata[0].content.split(" ")[0].delete(",").to_i64
published = Time.now
else
published = decode_date(metadata[0].content)
view_count = metadata[1].content.split(" ")[0]
if view_count == "No"
view_count = 0_i64
else
view_count = view_count.delete(",").to_i64
end
end
description_html = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-description")]))
if !description_html
description = ""
description_html = ""
else
description_html = description_html.to_s
description = description_html.gsub("
", "\n")
description = description.gsub("
", "\n")
description = XML.parse_html(description).content.strip("\n ")
end
length_seconds = node.xpath_node(%q(.//span[@class="video-time"]))
if length_seconds
length_seconds = decode_length_seconds(length_seconds.content)
else
length_seconds = -1
end
video = SearchVideo.new(
title,
video_id,
author,
ucid,
published,
view_count,
description,
description_html,
length_seconds,
)
videos << video
end
return videos
end
def build_search_params(sort_by = "relevance", date : String = "", content_type : String = "", duration : String = "", features : Array(String) = [] of String)
head = "\x08"
head += case sort_by
when "relevance"
"\x00"
when "rating"
"\x01"
when "upload_date"
"\x02"
when "view_count"
"\x03"
else
raise "No sort #{sort_by}"
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"
else
""
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"
when "creative_commons"
"\x30\x01"
when "3d"
"\x38\x01"
when "live"
"\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
token = head + "\x12" + body.size.to_u8.unsafe_chr + body
else
token = head
end
token = Base64.urlsafe_encode(token)
token = URI.escape(token)
return token
end