forked from midou/invidious
Add mixes
This commit is contained in:
parent
66f3ab0663
commit
20130db556
@ -390,6 +390,7 @@ get "/embed/:id" do |env|
|
||||
end
|
||||
|
||||
# Playlists
|
||||
|
||||
get "/playlist" do |env|
|
||||
plid = env.params.query["list"]?
|
||||
if !plid
|
||||
@ -415,6 +416,25 @@ get "/playlist" do |env|
|
||||
templated "playlist"
|
||||
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
|
||||
|
||||
get "/results" do |env|
|
||||
@ -2166,12 +2186,13 @@ get "/api/v1/insights/:id" do |env|
|
||||
end
|
||||
|
||||
get "/api/v1/videos/:id" do |env|
|
||||
env.response.content_type = "application/json"
|
||||
|
||||
id = env.params.url["id"]
|
||||
|
||||
begin
|
||||
video = get_video(id, PG_DB, proxies)
|
||||
rescue ex
|
||||
env.response.content_type = "application/json"
|
||||
error_message = {"error" => ex.message}.to_json
|
||||
halt env, status_code: 500, response: error_message
|
||||
end
|
||||
@ -2181,7 +2202,6 @@ get "/api/v1/videos/:id" do |env|
|
||||
|
||||
captions = video.captions
|
||||
|
||||
env.response.content_type = "application/json"
|
||||
video_info = JSON.build do |json|
|
||||
json.object do
|
||||
json.field "title", video.title
|
||||
@ -2945,6 +2965,55 @@ get "/api/v1/playlists/:plid" do |env|
|
||||
response
|
||||
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|
|
||||
env.response.headers["Access-Control-Allow-Origin"] = "*"
|
||||
env.redirect "/videoplayback?#{env.params.query}"
|
||||
|
@ -244,11 +244,22 @@ def extract_items(nodeset, ucid = nil)
|
||||
plid = HTTP::Params.parse(URI.parse(id).query.not_nil!)["list"]
|
||||
|
||||
anchor = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-meta")]/a))
|
||||
|
||||
if !anchor
|
||||
anchor = node.xpath_node(%q(.//ul[@class="yt-lockup-meta-info"]/li/a))
|
||||
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
|
||||
video_count ||= 0
|
||||
|
||||
|
74
src/invidious/mixes.cr
Normal file
74
src/invidious/mixes.cr
Normal 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
|
@ -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
|
||||
add_mapping({
|
||||
title: String,
|
||||
@ -13,19 +26,6 @@ class Playlist
|
||||
})
|
||||
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)
|
||||
client = make_client(YT_URL)
|
||||
|
||||
|
@ -14,7 +14,12 @@
|
||||
<p><%= number_with_separator(item.subscriber_count) %> subscribers</p>
|
||||
<h5><%= item.description_html %></h5>
|
||||
<% 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 %>
|
||||
<% else %>
|
||||
<img style="width:100%;" src="/vi/<%= item.videos[0]?.try &.id %>/mqdefault.jpg"/>
|
||||
@ -26,6 +31,17 @@
|
||||
</p>
|
||||
<p><%= number_with_separator(item.video_count) %> videos</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 %>
|
||||
<% if item.responds_to?(:playlists) && !item.playlists.empty? %>
|
||||
<% params = "&list=#{item.playlists[0]}" %>
|
||||
|
22
src/invidious/views/mix.ecr
Normal file
22
src/invidious/views/mix.ecr
Normal 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 %>
|
Loading…
Reference in New Issue
Block a user