Skip to content

Commit 20130db

Browse files
committed
Add mixes
1 parent 66f3ab0 commit 20130db

File tree

6 files changed

+210
-18
lines changed

6 files changed

+210
-18
lines changed

src/invidious.cr

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ get "/embed/:id" do |env|
390390
end
391391

392392
# Playlists
393+
393394
get "/playlist" do |env|
394395
plid = env.params.query["list"]?
395396
if !plid
@@ -415,6 +416,25 @@ get "/playlist" do |env|
415416
templated "playlist"
416417
end
417418

419+
get "/mix" do |env|
420+
rdid = env.params.query["list"]?
421+
if !rdid
422+
next env.redirect "/"
423+
end
424+
425+
continuation = env.params.query["continuation"]?
426+
continuation ||= rdid.lchop("RD")
427+
428+
begin
429+
mix = fetch_mix(rdid, continuation)
430+
rescue ex
431+
error_message = ex.message
432+
next templated "error"
433+
end
434+
435+
templated "mix"
436+
end
437+
418438
# Search
419439

420440
get "/results" do |env|
@@ -2166,12 +2186,13 @@ get "/api/v1/insights/:id" do |env|
21662186
end
21672187

21682188
get "/api/v1/videos/:id" do |env|
2189+
env.response.content_type = "application/json"
2190+
21692191
id = env.params.url["id"]
21702192

21712193
begin
21722194
video = get_video(id, PG_DB, proxies)
21732195
rescue ex
2174-
env.response.content_type = "application/json"
21752196
error_message = {"error" => ex.message}.to_json
21762197
halt env, status_code: 500, response: error_message
21772198
end
@@ -2181,7 +2202,6 @@ get "/api/v1/videos/:id" do |env|
21812202

21822203
captions = video.captions
21832204

2184-
env.response.content_type = "application/json"
21852205
video_info = JSON.build do |json|
21862206
json.object do
21872207
json.field "title", video.title
@@ -2945,6 +2965,55 @@ get "/api/v1/playlists/:plid" do |env|
29452965
response
29462966
end
29472967

2968+
get "/api/v1/mixes/:rdid" do |env|
2969+
env.response.content_type = "application/json"
2970+
2971+
rdid = env.params.url["rdid"]
2972+
2973+
continuation = env.params.query["continuation"]?
2974+
continuation ||= rdid.lchop("RD")
2975+
2976+
begin
2977+
mix = fetch_mix(rdid, continuation)
2978+
rescue ex
2979+
error_message = {"error" => ex.message}.to_json
2980+
halt env, status_code: 500, response: error_message
2981+
end
2982+
2983+
response = JSON.build do |json|
2984+
json.object do
2985+
json.field "title", mix.title
2986+
json.field "mixId", mix.id
2987+
2988+
json.field "videos" do
2989+
json.array do
2990+
mix.videos.each do |video|
2991+
json.object do
2992+
json.field "title", video.title
2993+
json.field "videoId", video.id
2994+
json.field "author", video.author
2995+
2996+
json.field "authorId", video.ucid
2997+
json.field "authorUrl", "/channel/#{video.ucid}"
2998+
2999+
json.field "videoThumbnails" do
3000+
json.array do
3001+
generate_thumbnails(json, video.id)
3002+
end
3003+
end
3004+
3005+
json.field "index", video.index
3006+
json.field "lengthSeconds", video.length_seconds
3007+
end
3008+
end
3009+
end
3010+
end
3011+
end
3012+
end
3013+
3014+
response
3015+
end
3016+
29483017
get "/api/manifest/dash/id/videoplayback" do |env|
29493018
env.response.headers["Access-Control-Allow-Origin"] = "*"
29503019
env.redirect "/videoplayback?#{env.params.query}"

src/invidious/helpers/helpers.cr

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,11 +244,22 @@ def extract_items(nodeset, ucid = nil)
244244
plid = HTTP::Params.parse(URI.parse(id).query.not_nil!)["list"]
245245

246246
anchor = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-meta")]/a))
247+
247248
if !anchor
248249
anchor = node.xpath_node(%q(.//ul[@class="yt-lockup-meta-info"]/li/a))
249250
end
250-
if anchor
251-
video_count = anchor.content.match(/View full playlist \((?<count>\d+)/).try &.["count"].to_i?
251+
252+
video_count = node.xpath_node(%q(.//span[@class="formatted-video-count-label"]/b))
253+
if video_count
254+
video_count = video_count.content
255+
256+
if video_count == "50+"
257+
author = "YouTube"
258+
author_id = "UC-9-kyTW8ZkZNDHQJ6FgpwQ"
259+
video_count = video_count.rchop("+")
260+
end
261+
262+
video_count = video_count.to_i?
252263
end
253264
video_count ||= 0
254265

src/invidious/mixes.cr

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
class MixVideo
2+
add_mapping({
3+
title: String,
4+
id: String,
5+
author: String,
6+
ucid: String,
7+
length_seconds: Int32,
8+
index: Int32,
9+
})
10+
end
11+
12+
class Mix
13+
add_mapping({
14+
title: String,
15+
id: String,
16+
videos: Array(MixVideo),
17+
})
18+
end
19+
20+
def fetch_mix(rdid, video_id, cookies = nil)
21+
client = make_client(YT_URL)
22+
headers = HTTP::Headers.new
23+
headers["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36"
24+
25+
if cookies
26+
headers = cookies.add_request_headers(headers)
27+
end
28+
response = client.get("/watch?v=#{video_id}&list=#{rdid}&bpctr=#{Time.new.epoch + 2000}&gl=US&hl=en", headers)
29+
30+
yt_data = response.body.match(/window\["ytInitialData"\] = (?<data>.*);/)
31+
if yt_data
32+
yt_data = JSON.parse(yt_data["data"].rchop(";"))
33+
else
34+
raise "Could not create mix."
35+
end
36+
37+
playlist = yt_data["contents"]["twoColumnWatchNextResults"]["playlist"]["playlist"]
38+
mix_title = playlist["title"].as_s
39+
40+
contents = playlist["contents"].as_a
41+
until contents[0]["playlistPanelVideoRenderer"]["videoId"].as_s == video_id
42+
contents.shift
43+
end
44+
45+
videos = [] of MixVideo
46+
contents.each do |item|
47+
item = item["playlistPanelVideoRenderer"]
48+
49+
id = item["videoId"].as_s
50+
title = item["title"]["simpleText"].as_s
51+
author = item["longBylineText"]["runs"][0]["text"].as_s
52+
ucid = item["longBylineText"]["runs"][0]["navigationEndpoint"]["browseEndpoint"]["browseId"].as_s
53+
length_seconds = decode_length_seconds(item["lengthText"]["simpleText"].as_s)
54+
index = item["navigationEndpoint"]["watchEndpoint"]["index"].as_i
55+
56+
videos << MixVideo.new(
57+
title,
58+
id,
59+
author,
60+
ucid,
61+
length_seconds,
62+
index
63+
)
64+
end
65+
66+
if !cookies
67+
next_page = fetch_mix(rdid, videos[-1].id, response.cookies)
68+
videos += next_page.videos
69+
end
70+
71+
videos.uniq! { |video| video.id }
72+
videos = videos.first(50)
73+
return Mix.new(mix_title, rdid, videos)
74+
end

src/invidious/playlists.cr

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
class PlaylistVideo
2+
add_mapping({
3+
title: String,
4+
id: String,
5+
author: String,
6+
ucid: String,
7+
length_seconds: Int32,
8+
published: Time,
9+
playlists: Array(String),
10+
index: Int32,
11+
})
12+
end
13+
114
class Playlist
215
add_mapping({
316
title: String,
@@ -13,19 +26,6 @@ class Playlist
1326
})
1427
end
1528

16-
class PlaylistVideo
17-
add_mapping({
18-
title: String,
19-
id: String,
20-
author: String,
21-
ucid: String,
22-
length_seconds: Int32,
23-
published: Time,
24-
playlists: Array(String),
25-
index: Int32,
26-
})
27-
end
28-
2929
def fetch_playlist_videos(plid, page, video_count)
3030
client = make_client(YT_URL)
3131

src/invidious/views/components/item.ecr

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@
1414
<p><%= number_with_separator(item.subscriber_count) %> subscribers</p>
1515
<h5><%= item.description_html %></h5>
1616
<% when SearchPlaylist %>
17-
<a style="width:100%;" href="/playlist?list=<%= item.id %>">
17+
<% if item.id.starts_with? "RD" %>
18+
<% url = "/mix?list=#{item.id}&continuation=#{item.videos[0]?.try &.id}" %>
19+
<% else %>
20+
<% url = "/playlist?list=#{item.id}" %>
21+
<% end %>
22+
<a style="width:100%;" href="<%= url %>">
1823
<% if env.get?("user") && env.get("user").as(User).preferences.thin_mode %>
1924
<% else %>
2025
<img style="width:100%;" src="/vi/<%= item.videos[0]?.try &.id %>/mqdefault.jpg"/>
@@ -26,6 +31,17 @@
2631
</p>
2732
<p><%= number_with_separator(item.video_count) %> videos</p>
2833
<p>PLAYLIST</p>
34+
<% when MixVideo %>
35+
<a style="width:100%;" href="/watch?v=<%= item.id %>">
36+
<% if env.get?("user") && env.get("user").as(User).preferences.thin_mode %>
37+
<% else %>
38+
<img style="width:100%;" src="/vi/<%= item.id %>/mqdefault.jpg"/>
39+
<% end %>
40+
<p><%= item.title %></p>
41+
</a>
42+
<p>
43+
<b><a style="width:100%;" href="/channel/<%= item.ucid %>"><%= item.author %></a></b>
44+
</p>
2945
<% else %>
3046
<% if item.responds_to?(:playlists) && !item.playlists.empty? %>
3147
<% params = "&list=#{item.playlists[0]}" %>

src/invidious/views/mix.ecr

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<% content_for "header" do %>
2+
<title><%= mix.title %> - Invidious</title>
3+
<% end %>
4+
5+
<div class="pure-g h-box">
6+
<div class="pure-u-2-3">
7+
<h3><%= mix.title %></h3>
8+
</div>
9+
<div class="pure-u-1-3" style="text-align:right;">
10+
<h3>
11+
<a href="/feed/playlist/<%= mix.id %>"><i class="icon ion-logo-rss"></i></a>
12+
</h3>
13+
</div>
14+
</div>
15+
16+
<% mix.videos.each_slice(4) do |slice| %>
17+
<div class="pure-g">
18+
<% slice.each do |item| %>
19+
<%= rendered "components/item" %>
20+
<% end %>
21+
</div>
22+
<% end %>

0 commit comments

Comments
 (0)