From 20130db556c575e4434b1ce8a1429bc8b4ddbdf2 Mon Sep 17 00:00:00 2001
From: Omar Roth
Date: Fri, 28 Sep 2018 23:12:35 -0500
Subject: [PATCH] Add mixes
---
src/invidious.cr | 73 +++++++++++++++++++++++-
src/invidious/helpers/helpers.cr | 15 ++++-
src/invidious/mixes.cr | 74 +++++++++++++++++++++++++
src/invidious/playlists.cr | 26 ++++-----
src/invidious/views/components/item.ecr | 18 +++++-
src/invidious/views/mix.ecr | 22 ++++++++
6 files changed, 210 insertions(+), 18 deletions(-)
create mode 100644 src/invidious/mixes.cr
create mode 100644 src/invidious/views/mix.ecr
diff --git a/src/invidious.cr b/src/invidious.cr
index dc61c105..383a12d7 100644
--- a/src/invidious.cr
+++ b/src/invidious.cr
@@ -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}"
diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr
index 906d9fa5..ab33c3af 100644
--- a/src/invidious/helpers/helpers.cr
+++ b/src/invidious/helpers/helpers.cr
@@ -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 \((?\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
diff --git a/src/invidious/mixes.cr b/src/invidious/mixes.cr
new file mode 100644
index 00000000..e7e76b80
--- /dev/null
+++ b/src/invidious/mixes.cr
@@ -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"\] = (?.*);/)
+ 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
diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr
index 0626f4ae..32fcd016 100644
--- a/src/invidious/playlists.cr
+++ b/src/invidious/playlists.cr
@@ -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)
diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr
index 734f47f7..1ffc6467 100644
--- a/src/invidious/views/components/item.ecr
+++ b/src/invidious/views/components/item.ecr
@@ -14,7 +14,12 @@
<%= number_with_separator(item.subscriber_count) %> subscribers
<%= item.description_html %>
<% when SearchPlaylist %>
-
+ <% if item.id.starts_with? "RD" %>
+ <% url = "/mix?list=#{item.id}&continuation=#{item.videos[0]?.try &.id}" %>
+ <% else %>
+ <% url = "/playlist?list=#{item.id}" %>
+ <% end %>
+
<% if env.get?("user") && env.get("user").as(User).preferences.thin_mode %>
<% else %>
@@ -26,6 +31,17 @@
<%= number_with_separator(item.video_count) %> videos
PLAYLIST
+ <% when MixVideo %>
+
+ <% if env.get?("user") && env.get("user").as(User).preferences.thin_mode %>
+ <% else %>
+
+ <% end %>
+ <%= item.title %>
+
+
+ <%= item.author %>
+
<% else %>
<% if item.responds_to?(:playlists) && !item.playlists.empty? %>
<% params = "&list=#{item.playlists[0]}" %>
diff --git a/src/invidious/views/mix.ecr b/src/invidious/views/mix.ecr
new file mode 100644
index 00000000..139e01b9
--- /dev/null
+++ b/src/invidious/views/mix.ecr
@@ -0,0 +1,22 @@
+<% content_for "header" do %>
+<%= mix.title %> - Invidious
+<% end %>
+
+
+
+
<%= mix.title %>
+
+
+
+
+<% mix.videos.each_slice(4) do |slice| %>
+
+ <% slice.each do |item| %>
+ <%= rendered "components/item" %>
+ <% end %>
+
+<% end %>