invidious-copy-2022-04-11/src/invidious/playlists.cr

262 lines
7 KiB
Crystal
Raw Normal View History

2019-03-29 21:30:02 +00:00
struct PlaylistVideo
db_mapping({
2018-09-29 04:12:35 +00:00
title: String,
id: String,
author: String,
ucid: String,
length_seconds: Int32,
published: Time,
playlists: Array(String),
index: Int32,
2019-03-24 14:10:14 +00:00
live_now: Bool,
2018-09-29 04:12:35 +00:00
})
end
2019-03-29 21:30:02 +00:00
struct Playlist
db_mapping({
2018-09-05 00:27:10 +00:00
title: String,
id: String,
author: String,
author_thumbnail: String,
2018-09-05 00:27:10 +00:00
ucid: String,
description: String,
description_html: String,
video_count: Int32,
views: Int64,
updated: Time,
2018-08-15 15:22:36 +00:00
})
end
2018-12-20 21:32:09 +00:00
def fetch_playlist_videos(plid, page, video_count, continuation = nil, locale = nil)
2018-08-15 15:22:36 +00:00
client = make_client(YT_URL)
2018-10-08 02:11:33 +00:00
if continuation
2018-11-10 16:50:09 +00:00
html = client.get("/watch?v=#{continuation}&list=#{plid}&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999")
2018-10-08 02:11:33 +00:00
html = XML.parse_html(html.body)
index = html.xpath_node(%q(//span[@id="playlist-current-index"])).try &.content.to_i?
if index
index -= 1
end
index ||= 0
else
index = (page - 1) * 100
2018-10-08 02:11:33 +00:00
end
if video_count > 100
url = produce_playlist_url(plid, index)
response = client.get(url)
response = JSON.parse(response.body)
if !response["content_html"]? || response["content_html"].as_s.empty?
2018-12-20 21:32:09 +00:00
raise translate(locale, "Playlist is empty")
end
document = XML.parse_html(response["content_html"].as_s)
nodeset = document.xpath_nodes(%q(.//tr[contains(@class, "pl-video")]))
videos = extract_playlist(plid, nodeset, index)
else
2018-09-28 14:54:01 +00:00
# Playlist has less than one page of videos, so subsequent pages will be empty
if page > 1
videos = [] of PlaylistVideo
else
2018-09-28 14:54:01 +00:00
# Extract first page of videos
2018-09-25 22:55:32 +00:00
response = client.get("/playlist?list=#{plid}&gl=US&hl=en&disable_polymer=1")
document = XML.parse_html(response.body)
nodeset = document.xpath_nodes(%q(.//tr[contains(@class, "pl-video")]))
videos = extract_playlist(plid, nodeset, 0)
if continuation
until videos[0].id == continuation
videos.shift
end
end
end
2018-08-15 15:22:36 +00:00
end
return videos
end
def extract_playlist(plid, nodeset, index)
2018-08-15 15:22:36 +00:00
videos = [] of PlaylistVideo
nodeset.each_with_index do |video, offset|
anchor = video.xpath_node(%q(.//td[@class="pl-video-title"]))
if !anchor
next
2018-08-15 15:22:36 +00:00
end
title = anchor.xpath_node(%q(.//a)).not_nil!.content.strip(" \n")
id = anchor.xpath_node(%q(.//a)).not_nil!["href"].lchop("/watch?v=")[0, 11]
anchor = anchor.xpath_node(%q(.//div[@class="pl-video-owner"]/a))
if anchor
author = anchor.content
ucid = anchor["href"].split("/")[2]
else
author = ""
ucid = ""
end
anchor = video.xpath_node(%q(.//td[@class="pl-video-time"]/div/div[1]))
if anchor && !anchor.content.empty?
length_seconds = decode_length_seconds(anchor.content)
2019-03-24 14:10:14 +00:00
live_now = false
else
length_seconds = 0
2019-03-24 14:10:14 +00:00
live_now = true
end
videos << PlaylistVideo.new(
2018-12-20 21:32:09 +00:00
title: title,
id: id,
author: author,
ucid: ucid,
length_seconds: length_seconds,
published: Time.now,
playlists: [plid],
index: index + offset,
2019-03-24 14:10:14 +00:00
live_now: live_now
)
2018-08-15 15:22:36 +00:00
end
return videos
end
def produce_playlist_url(id, index)
if id.starts_with? "UC"
id = "UU" + id.lchop("UC")
end
ucid = "VL" + id
meta = IO::Memory.new
meta.write(Bytes[0x08])
meta.write(write_var_int(index))
meta.rewind
meta = Base64.urlsafe_encode(meta.to_slice, false)
2018-09-17 21:38:18 +00:00
meta = "PT:#{meta}"
continuation = IO::Memory.new
continuation.write(Bytes[0x7a, meta.size])
continuation.print(meta)
2018-09-17 21:38:18 +00:00
continuation.rewind
meta = Base64.urlsafe_encode(continuation.to_slice)
meta = URI.escape(meta)
2018-09-17 21:38:18 +00:00
continuation = IO::Memory.new
continuation.write(Bytes[0x12, ucid.size])
continuation.print(ucid)
continuation.write(Bytes[0x1a, meta.size])
continuation.print(meta)
2018-09-17 21:38:18 +00:00
wrapper = IO::Memory.new
wrapper.write(Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02, continuation.size])
wrapper.print(continuation)
wrapper.rewind
2018-09-17 21:38:18 +00:00
wrapper = Base64.urlsafe_encode(wrapper.to_slice)
wrapper = URI.escape(wrapper)
2018-08-15 15:22:36 +00:00
url = "/browse_ajax?continuation=#{wrapper}&gl=US&hl=en"
2018-08-15 15:22:36 +00:00
return url
end
2018-12-20 21:32:09 +00:00
def fetch_playlist(plid, locale)
2018-08-15 15:22:36 +00:00
client = make_client(YT_URL)
if plid.starts_with? "UC"
plid = "UU#{plid.lchop("UC")}"
end
2018-09-25 22:55:32 +00:00
response = client.get("/playlist?list=#{plid}&hl=en&disable_polymer=1")
2018-09-23 17:26:12 +00:00
if response.status_code != 200
2018-12-20 21:32:09 +00:00
raise translate(locale, "Invalid playlist.")
2018-09-23 17:26:12 +00:00
end
2019-01-05 04:48:00 +00:00
body = response.body.gsub(/<button[^>]+><span[^>]+>\s*less\s*<img[^>]+>\n<\/span><\/button>/, "")
document = XML.parse_html(body)
2018-08-15 15:22:36 +00:00
2018-09-23 17:32:32 +00:00
title = document.xpath_node(%q(//h1[@class="pl-header-title"]))
if !title
2018-12-20 21:32:09 +00:00
raise translate(locale, "Playlist does not exist.")
2018-09-23 17:32:32 +00:00
end
title = title.content.strip(" \n")
2018-08-15 15:22:36 +00:00
2018-09-05 00:27:10 +00:00
description_html = document.xpath_node(%q(//span[@class="pl-header-description-text"]/div/div[1]))
description_html ||= document.xpath_node(%q(//span[@class="pl-header-description-text"]))
description_html, description = html_to_content(description_html)
2018-08-15 15:22:36 +00:00
anchor = document.xpath_node(%q(//ul[@class="pl-header-details"])).not_nil!
author = anchor.xpath_node(%q(.//li[1]/a)).not_nil!.content
author_thumbnail = document.xpath_node(%q(//img[@class="channel-header-profile-image"])).try &.["src"]
author_thumbnail ||= ""
ucid = anchor.xpath_node(%q(.//li[1]/a)).not_nil!["href"].split("/")[-1]
2018-08-15 15:22:36 +00:00
2018-08-20 15:25:05 +00:00
video_count = anchor.xpath_node(%q(.//li[2])).not_nil!.content.delete("videos, ").to_i
2018-09-18 00:21:13 +00:00
views = anchor.xpath_node(%q(.//li[3])).not_nil!.content.delete("No views, ")
if views.empty?
views = 0_i64
else
views = views.to_i64
end
2018-08-15 15:22:36 +00:00
updated = anchor.xpath_node(%q(.//li[4])).not_nil!.content.lchop("Last updated on ").lchop("Updated ")
updated = decode_date(updated)
playlist = Playlist.new(
2018-12-15 19:02:53 +00:00
title: title,
id: plid,
author: author,
author_thumbnail: author_thumbnail,
ucid: ucid,
description: description,
description_html: description_html,
video_count: video_count,
views: views,
updated: updated
2018-08-15 15:22:36 +00:00
)
return playlist
end
2018-10-08 02:11:33 +00:00
def template_playlist(playlist)
html = <<-END_HTML
<h3>
<a href="/playlist?list=#{playlist["playlistId"]}">
#{playlist["title"]}
</a>
</h3>
<div class="pure-menu pure-menu-scrollable playlist-restricted">
<ol class="pure-menu-list">
END_HTML
playlist["videos"].as_a.each do |video|
html += <<-END_HTML
<li class="pure-menu-item">
<a href="/watch?v=#{video["videoId"]}&list=#{playlist["playlistId"]}">
<div class="thumbnail">
<img class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg">
<p class="length">#{recode_length_seconds(video["lengthSeconds"].as_i)}</p>
</div>
2018-10-08 02:11:33 +00:00
<p style="width:100%">#{video["title"]}</p>
<p>
<b style="width: 100%">#{video["author"]}</b>
</p>
</a>
</li>
END_HTML
end
html += <<-END_HTML
</ol>
</div>
<hr>
END_HTML
html
end