Add mixes

This commit is contained in:
Omar Roth 2018-09-28 23:12:35 -05:00
parent 66f3ab0663
commit 20130db556
6 changed files with 210 additions and 18 deletions

View File

@ -390,6 +390,7 @@ get "/embed/:id" do |env|
end end
# Playlists # Playlists
get "/playlist" do |env| get "/playlist" do |env|
plid = env.params.query["list"]? plid = env.params.query["list"]?
if !plid if !plid
@ -415,6 +416,25 @@ get "/playlist" do |env|
templated "playlist" templated "playlist"
end end
get "/mix" do |env|
rdid = env.params.query["list"]?
if !rdid
next env.redirect "/"
end
continuation = env.params.query["continuation"]?
continuation ||= rdid.lchop("RD")
begin
mix = fetch_mix(rdid, continuation)
rescue ex
error_message = ex.message
next templated "error"
end
templated "mix"
end
# Search # Search
get "/results" do |env| get "/results" do |env|
@ -2166,12 +2186,13 @@ get "/api/v1/insights/:id" do |env|
end end
get "/api/v1/videos/:id" do |env| get "/api/v1/videos/:id" do |env|
env.response.content_type = "application/json"
id = env.params.url["id"] id = env.params.url["id"]
begin begin
video = get_video(id, PG_DB, proxies) video = get_video(id, PG_DB, proxies)
rescue ex rescue ex
env.response.content_type = "application/json"
error_message = {"error" => ex.message}.to_json error_message = {"error" => ex.message}.to_json
halt env, status_code: 500, response: error_message halt env, status_code: 500, response: error_message
end end
@ -2181,7 +2202,6 @@ get "/api/v1/videos/:id" do |env|
captions = video.captions captions = video.captions
env.response.content_type = "application/json"
video_info = JSON.build do |json| video_info = JSON.build do |json|
json.object do json.object do
json.field "title", video.title json.field "title", video.title
@ -2945,6 +2965,55 @@ get "/api/v1/playlists/:plid" do |env|
response response
end end
get "/api/v1/mixes/:rdid" do |env|
env.response.content_type = "application/json"
rdid = env.params.url["rdid"]
continuation = env.params.query["continuation"]?
continuation ||= rdid.lchop("RD")
begin
mix = fetch_mix(rdid, continuation)
rescue ex
error_message = {"error" => ex.message}.to_json
halt env, status_code: 500, response: error_message
end
response = JSON.build do |json|
json.object do
json.field "title", mix.title
json.field "mixId", mix.id
json.field "videos" do
json.array do
mix.videos.each do |video|
json.object do
json.field "title", video.title
json.field "videoId", video.id
json.field "author", video.author
json.field "authorId", video.ucid
json.field "authorUrl", "/channel/#{video.ucid}"
json.field "videoThumbnails" do
json.array do
generate_thumbnails(json, video.id)
end
end
json.field "index", video.index
json.field "lengthSeconds", video.length_seconds
end
end
end
end
end
end
response
end
get "/api/manifest/dash/id/videoplayback" do |env| get "/api/manifest/dash/id/videoplayback" do |env|
env.response.headers["Access-Control-Allow-Origin"] = "*" env.response.headers["Access-Control-Allow-Origin"] = "*"
env.redirect "/videoplayback?#{env.params.query}" env.redirect "/videoplayback?#{env.params.query}"

View File

@ -244,11 +244,22 @@ def extract_items(nodeset, ucid = nil)
plid = HTTP::Params.parse(URI.parse(id).query.not_nil!)["list"] plid = HTTP::Params.parse(URI.parse(id).query.not_nil!)["list"]
anchor = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-meta")]/a)) anchor = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-meta")]/a))
if !anchor if !anchor
anchor = node.xpath_node(%q(.//ul[@class="yt-lockup-meta-info"]/li/a)) anchor = node.xpath_node(%q(.//ul[@class="yt-lockup-meta-info"]/li/a))
end end
if anchor
video_count = anchor.content.match(/View full playlist \((?<count>\d+)/).try &.["count"].to_i? video_count = node.xpath_node(%q(.//span[@class="formatted-video-count-label"]/b))
if video_count
video_count = video_count.content
if video_count == "50+"
author = "YouTube"
author_id = "UC-9-kyTW8ZkZNDHQJ6FgpwQ"
video_count = video_count.rchop("+")
end
video_count = video_count.to_i?
end end
video_count ||= 0 video_count ||= 0

74
src/invidious/mixes.cr Normal file
View File

@ -0,0 +1,74 @@
class MixVideo
add_mapping({
title: String,
id: String,
author: String,
ucid: String,
length_seconds: Int32,
index: Int32,
})
end
class Mix
add_mapping({
title: String,
id: String,
videos: Array(MixVideo),
})
end
def fetch_mix(rdid, video_id, cookies = nil)
client = make_client(YT_URL)
headers = HTTP::Headers.new
headers["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36"
if cookies
headers = cookies.add_request_headers(headers)
end
response = client.get("/watch?v=#{video_id}&list=#{rdid}&bpctr=#{Time.new.epoch + 2000}&gl=US&hl=en", headers)
yt_data = response.body.match(/window\["ytInitialData"\] = (?<data>.*);/)
if yt_data
yt_data = JSON.parse(yt_data["data"].rchop(";"))
else
raise "Could not create mix."
end
playlist = yt_data["contents"]["twoColumnWatchNextResults"]["playlist"]["playlist"]
mix_title = playlist["title"].as_s
contents = playlist["contents"].as_a
until contents[0]["playlistPanelVideoRenderer"]["videoId"].as_s == video_id
contents.shift
end
videos = [] of MixVideo
contents.each do |item|
item = item["playlistPanelVideoRenderer"]
id = item["videoId"].as_s
title = item["title"]["simpleText"].as_s
author = item["longBylineText"]["runs"][0]["text"].as_s
ucid = item["longBylineText"]["runs"][0]["navigationEndpoint"]["browseEndpoint"]["browseId"].as_s
length_seconds = decode_length_seconds(item["lengthText"]["simpleText"].as_s)
index = item["navigationEndpoint"]["watchEndpoint"]["index"].as_i
videos << MixVideo.new(
title,
id,
author,
ucid,
length_seconds,
index
)
end
if !cookies
next_page = fetch_mix(rdid, videos[-1].id, response.cookies)
videos += next_page.videos
end
videos.uniq! { |video| video.id }
videos = videos.first(50)
return Mix.new(mix_title, rdid, videos)
end

View File

@ -1,3 +1,16 @@
class PlaylistVideo
add_mapping({
title: String,
id: String,
author: String,
ucid: String,
length_seconds: Int32,
published: Time,
playlists: Array(String),
index: Int32,
})
end
class Playlist class Playlist
add_mapping({ add_mapping({
title: String, title: String,
@ -13,19 +26,6 @@ class Playlist
}) })
end 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 fetch_playlist_videos(plid, page, video_count) def fetch_playlist_videos(plid, page, video_count)
client = make_client(YT_URL) client = make_client(YT_URL)

View File

@ -14,7 +14,12 @@
<p><%= number_with_separator(item.subscriber_count) %> subscribers</p> <p><%= number_with_separator(item.subscriber_count) %> subscribers</p>
<h5><%= item.description_html %></h5> <h5><%= item.description_html %></h5>
<% when SearchPlaylist %> <% when SearchPlaylist %>
<a style="width:100%;" href="/playlist?list=<%= item.id %>"> <% if item.id.starts_with? "RD" %>
<% url = "/mix?list=#{item.id}&continuation=#{item.videos[0]?.try &.id}" %>
<% else %>
<% url = "/playlist?list=#{item.id}" %>
<% end %>
<a style="width:100%;" href="<%= url %>">
<% if env.get?("user") && env.get("user").as(User).preferences.thin_mode %> <% if env.get?("user") && env.get("user").as(User).preferences.thin_mode %>
<% else %> <% else %>
<img style="width:100%;" src="/vi/<%= item.videos[0]?.try &.id %>/mqdefault.jpg"/> <img style="width:100%;" src="/vi/<%= item.videos[0]?.try &.id %>/mqdefault.jpg"/>
@ -26,6 +31,17 @@
</p> </p>
<p><%= number_with_separator(item.video_count) %> videos</p> <p><%= number_with_separator(item.video_count) %> videos</p>
<p>PLAYLIST</p> <p>PLAYLIST</p>
<% when MixVideo %>
<a style="width:100%;" href="/watch?v=<%= item.id %>">
<% if env.get?("user") && env.get("user").as(User).preferences.thin_mode %>
<% else %>
<img style="width:100%;" src="/vi/<%= item.id %>/mqdefault.jpg"/>
<% end %>
<p><%= item.title %></p>
</a>
<p>
<b><a style="width:100%;" href="/channel/<%= item.ucid %>"><%= item.author %></a></b>
</p>
<% else %> <% else %>
<% if item.responds_to?(:playlists) && !item.playlists.empty? %> <% if item.responds_to?(:playlists) && !item.playlists.empty? %>
<% params = "&list=#{item.playlists[0]}" %> <% params = "&list=#{item.playlists[0]}" %>

View File

@ -0,0 +1,22 @@
<% content_for "header" do %>
<title><%= mix.title %> - Invidious</title>
<% end %>
<div class="pure-g h-box">
<div class="pure-u-2-3">
<h3><%= mix.title %></h3>
</div>
<div class="pure-u-1-3" style="text-align:right;">
<h3>
<a href="/feed/playlist/<%= mix.id %>"><i class="icon ion-logo-rss"></i></a>
</h3>
</div>
</div>
<% mix.videos.each_slice(4) do |slice| %>
<div class="pure-g">
<% slice.each do |item| %>
<%= rendered "components/item" %>
<% end %>
</div>
<% end %>