Add playlist page and endpoint

This commit is contained in:
Omar Roth 2018-08-15 10:22:36 -05:00
parent fa2ba807a3
commit bb0b60e575
7 changed files with 311 additions and 59 deletions

View file

@ -262,7 +262,7 @@ def fill_links(html, scheme, host)
end
if host == "www.youtube.com"
html = html.xpath_node(%q(//p[@id="eow-description"])).not_nil!.to_xml
html = html.xpath_node(%q(//body)).not_nil!.to_xml
else
html = html.to_xml(options: XML::SaveOptions::NO_DECL)
end

View file

@ -116,40 +116,6 @@ def login_req(login_form, f_req)
return HTTP::Params.encode(data)
end
def produce_playlist_url(id, index)
if id.starts_with? "UC"
id = "UU" + id.lchop("UC")
end
ucid = "VL" + id
continuation = [0x08_u8] + write_var_int(index)
slice = continuation.to_unsafe.to_slice(continuation.size)
slice = Base64.urlsafe_encode(slice, false)
# Inner Base64
continuation = "PT:" + slice
continuation = [0x7a_u8, continuation.bytes.size.to_u8] + continuation.bytes
slice = continuation.to_unsafe.to_slice(continuation.size)
slice = Base64.urlsafe_encode(slice)
slice = URI.escape(slice)
# Outer Base64
continuation = [0x1a.to_u8, slice.bytes.size.to_u8] + slice.bytes
continuation = ucid.bytes + continuation
continuation = [0x12_u8, ucid.size.to_u8] + continuation
continuation = [0xe2_u8, 0xa9_u8, 0x85_u8, 0xb2_u8, 2_u8, continuation.size.to_u8] + continuation
# Wrap bytes
slice = continuation.to_unsafe.to_slice(continuation.size)
slice = Base64.urlsafe_encode(slice)
slice = URI.escape(slice)
continuation = slice
url = "/browse_ajax?action_continuation=1&continuation=#{continuation}"
return url
end
def produce_videos_url(ucid, page = 1)
page = "#{page}"

View file

@ -64,10 +64,23 @@ end
def decode_date(string : String)
# String matches 'YYYY'
if string.match(/\d{4}/)
if string.match(/^\d{4}/)
return Time.new(string.to_i, 1, 1)
end
# Try to parse as format Jul 10, 2000
begin
return Time.parse(string, "%b %-d, %Y", Time::Location.local)
rescue ex
end
case string
when "today"
return Time.now
when "yesterday"
return Time.now - 1.day
end
# String matches format "20 hours ago", "4 months ago"...
date = string.split(" ")[-3, 3]
delta = date[0].to_i

160
src/invidious/playlists.cr Normal file
View file

@ -0,0 +1,160 @@
class Playlist
add_mapping({
title: String,
id: String,
author: String,
ucid: String,
description: String,
video_count: Int32,
views: Int64,
updated: Time,
})
end
class PlaylistVideo
add_mapping({
title: String,
id: String,
author: String,
ucid: String,
length_seconds: Int32,
published: Time,
playlists: Array(String),
index: Int32,
})
end
def extract_playlist(plid, page)
index = (page - 1) * 100
url = produce_playlist_url(plid, index)
client = make_client(YT_URL)
response = client.get(url)
response = JSON.parse(response.body)
if !response["content_html"]? || response["content_html"].as_s.empty?
raise "Playlist does not exist"
end
videos = [] of PlaylistVideo
document = XML.parse_html(response["content_html"].as_s)
anchor = document.xpath_node(%q(//div[@class="pl-video-owner"]/a))
if anchor
document.xpath_nodes(%q(.//tr[contains(@class, "pl-video")])).each_with_index do |video, offset|
anchor = video.xpath_node(%q(.//td[@class="pl-video-title"]))
if !anchor
next
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)
else
length_seconds = 0
end
videos << PlaylistVideo.new(
title,
id,
author,
ucid,
length_seconds,
Time.now,
[plid],
index + offset,
)
end
end
return videos
end
def produce_playlist_url(id, index)
if id.starts_with? "UC"
id = "UU" + id.lchop("UC")
end
ucid = "VL" + id
continuation = [0x08_u8] + write_var_int(index)
slice = continuation.to_unsafe.to_slice(continuation.size)
slice = Base64.urlsafe_encode(slice, false)
# Inner Base64
continuation = "PT:" + slice
continuation = [0x7a_u8, continuation.bytes.size.to_u8] + continuation.bytes
slice = continuation.to_unsafe.to_slice(continuation.size)
slice = Base64.urlsafe_encode(slice)
slice = URI.escape(slice)
# Outer Base64
continuation = [0x1a.to_u8, slice.bytes.size.to_u8] + slice.bytes
continuation = ucid.bytes + continuation
continuation = [0x12_u8, ucid.size.to_u8] + continuation
continuation = [0xe2_u8, 0xa9_u8, 0x85_u8, 0xb2_u8, 2_u8, continuation.size.to_u8] + continuation
# Wrap bytes
slice = continuation.to_unsafe.to_slice(continuation.size)
slice = Base64.urlsafe_encode(slice)
slice = URI.escape(slice)
continuation = slice
url = "/browse_ajax?action_continuation=1&continuation=#{continuation}"
return url
end
def fetch_playlist(plid)
client = make_client(YT_URL)
response = client.get("/playlist?list=#{plid}&disable_polymer=1")
document = XML.parse_html(response.body)
title = document.xpath_node(%q(//h1[@class="pl-header-title"])).not_nil!.content
title = title.strip(" \n")
description = document.xpath_node(%q(//span[@class="pl-header-description-text"]/div/div[1]))
description ||= document.xpath_node(%q(//span[@class="pl-header-description-text"]))
if description
description = description.to_xml.strip(" \n")
description = description.split("<button ")[0]
description = fill_links(description, "https", "www.youtube.com")
description = add_alt_links(description)
else
description = ""
end
anchor = document.xpath_node(%q(//ul[@class="pl-header-details"])).not_nil!
author = anchor.xpath_node(%q(.//li[1]/a)).not_nil!.content
ucid = anchor.xpath_node(%q(.//li[1]/a)).not_nil!["href"].split("/")[2]
video_count = anchor.xpath_node(%q(.//li[2])).not_nil!.content.delete("videos").to_i
views = anchor.xpath_node(%q(.//li[3])).not_nil!.content.delete("views,").to_i64
updated = anchor.xpath_node(%q(.//li[4])).not_nil!.content.lchop("Last updated on ").lchop("Updated ")
updated = decode_date(updated)
playlist = Playlist.new(
title,
plid,
author,
ucid,
description,
video_count,
views,
updated
)
return playlist
end

View file

@ -1,6 +1,11 @@
<div class="pure-u-1 pure-u-md-1-4">
<div class="h-box">
<a style="width:100%;" href="/watch?v=<%= video.id %>">
<% if video.responds_to?(:playlists) %>
<% params = "&list=#{video.playlists[0]}" %>
<% else %>
<% params = nil %>
<% end %>
<a style="width:100%;" href="/watch?v=<%= video.id %><%= params %>">
<% if env.get?("user") && env.get("user").as(User).preferences.thin_mode %>
<% else %>
<img style="width:100%;" src="https://i.ytimg.com/vi/<%= video.id %>/mqdefault.jpg"/>

View file

@ -0,0 +1,42 @@
<% content_for "header" do %>
<title><%= playlist.title %> - Invidious</title>
<% end %>
<div class="pure-g h-box">
<div class="pure-u-2-3">
<h3><%= playlist.title %></h3>
</div>
</div>
<div class="pure-g h-box">
<div class="pure-u-1 pure-u-md-1-4">
<a href="/channel/<%= playlist.ucid %>">
<b><%= playlist.author %></b>
</a>
</div>
</div>
<div class="h-box">
<p><%= playlist.description %></p>
</div>
<% videos.each_slice(4) do |slice| %>
<div class="pure-g">
<% slice.each do |video| %>
<%= rendered "components/video" %>
<% end %>
</div>
<% end %>
<div class="pure-g h-box">
<div class="pure-u-1 pure-u-md-1-5">
<% if page >= 2 %>
<a href="/playlist?list=<%= playlist.id %>&page=<%= page - 1 %>">Next page</a>
<% end %>
</div>
<div class="pure-u-1 pure-u-md-3-5"></div>
<div style="text-align:right;" class="pure-u-1 pure-u-md-1-5">
<% if videos.size == 100 %>
<a href="/playlist?list=<%= playlist.id %>&page=<%= page + 1 %>">Next page</a>
<% end %>
</div>
</div>