diff --git a/src/invidious.cr b/src/invidious.cr index dc61c1059..383a12d7d 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 906d9fa5f..ab33c3aff 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 000000000..e7e76b800 --- /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 0626f4ae6..32fcd0165 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 734f47f74..1ffc64673 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 000000000..139e01b94 --- /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 %>