mirror of
https://github.com/iv-org/invidious.git
synced 2024-12-19 12:44:23 -05:00
Add mixes
This commit is contained in:
parent
66f3ab0663
commit
20130db556
@ -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}"
|
||||||
|
@ -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
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
|
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)
|
||||||
|
|
||||||
|
@ -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]}" %>
|
||||||
|
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