From fec82df4516c48e27ef12ed7e48faf7e9590d332 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 18 Mar 2021 23:11:46 +0000 Subject: [PATCH 01/11] Fix fetching of large playlist --- src/invidious/playlists.cr | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index 0251a69c..feaed6de 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -441,17 +441,8 @@ def fetch_playlist_videos(plid, video_count, offset = 0, locale = nil, continuat offset = initial_data["currentVideoEndpoint"]?.try &.["watchEndpoint"]?.try &.["index"]?.try &.as_i64 || offset end - if video_count > 100 - url = produce_playlist_url(plid, offset) - - response = YT_POOL.client &.get(url) - initial_data = JSON.parse(response.body).as_a.find(&.as_h.["response"]?).try &.as_h - elsif offset > 100 - return [] of PlaylistVideo - else # Extract first page of videos - response = YT_POOL.client &.get("/playlist?list=#{plid}&gl=US&hl=en") - initial_data = extract_initial_data(response.body) - end + response = YT_POOL.client &.get("/playlist?list=#{plid}&gl=US&hl=en") + initial_data = extract_initial_data(response.body) return [] of PlaylistVideo if !initial_data videos = extract_playlist_videos(initial_data) From 89be1975ea6363b864eae1974c1e07fbdf90eeb4 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 20 Mar 2021 18:25:02 +0000 Subject: [PATCH 02/11] Playlist: Fix continuation token generation --- src/invidious/playlists.cr | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index feaed6de..71f8360d 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -307,23 +307,32 @@ def subscribe_playlist(db, user, playlist) return playlist end -def produce_playlist_url(id, index) +def produce_playlist_continuation(id, index) if id.starts_with? "UC" id = "UU" + id.lchop("UC") end plid = "VL" + id + # Emulate a "request counter" increment, to make perfectly valid + # ctokens, even if at the time of writing, it's ignored by youtube. + request_count = (index / 100).to_i64 || 1_i64 + data = {"1:varint" => index.to_i64} .try { |i| Protodec::Any.cast_json(i) } .try { |i| Protodec::Any.from_json(i) } .try { |i| Base64.urlsafe_encode(i, padding: false) } + data_wrapper = { "1:varint" => request_count, "15:string" => "PT:#{data}" } + .try { |i| Protodec::Any.cast_json(i) } + .try { |i| Protodec::Any.from_json(i) } + .try { |i| Base64.urlsafe_encode(i) } + .try { |i| URI.encode_www_form(i) } + object = { "80226972:embedded" => { "2:string" => plid, - "3:base64" => { - "15:string" => "PT:#{data}", - }, + "3:string" => data_wrapper, + "35:string" => id, }, } @@ -332,7 +341,7 @@ def produce_playlist_url(id, index) .try { |i| Base64.urlsafe_encode(i) } .try { |i| URI.encode_www_form(i) } - return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en" + return continuation end def get_playlist(db, plid, locale, refresh = true, force_refresh = false) From f99d62a2bcd5992656217f727beb25751e11d143 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 20 Mar 2021 20:57:18 +0000 Subject: [PATCH 03/11] Create youtube API wrapper fo /youtubei/v1/browse --- src/invidious/helpers/youtube_api.cr | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/invidious/helpers/youtube_api.cr diff --git a/src/invidious/helpers/youtube_api.cr b/src/invidious/helpers/youtube_api.cr new file mode 100644 index 00000000..0ae80318 --- /dev/null +++ b/src/invidious/helpers/youtube_api.cr @@ -0,0 +1,31 @@ +# +# This file contains youtube API wrappers +# + +# Hard-coded constants required by the API +HARDCODED_API_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8" +HARDCODED_CLIENT_VERS = "2.20210318.08.00" + +def request_youtube_api_browse(continuation) + # JSON Request data, required by the API + data = { + "context": { + "client": { + "hl": "en", + "gl": "US", + "clientName": "WEB", + "clientVersion": HARDCODED_CLIENT_VERS, + }, + }, + "continuation": continuation, + } + + # Send the POST request and return result + response = YT_POOL.client &.post( + "/youtubei/v1/browse?key=#{HARDCODED_API_KEY}", + headers: HTTP::Headers{"content-type" => "application/json"}, + body: data.to_json + ) + + return response.body +end From 980f5f129910fa2d2aca77b686469484f932680b Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 20 Mar 2021 22:12:06 +0000 Subject: [PATCH 04/11] Playlist: Fix video continuation (100+ videos playlists) --- src/invidious/playlists.cr | 66 ++++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index 71f8360d..1ef71a84 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -436,38 +436,56 @@ def fetch_playlist(plid, locale) end def get_playlist_videos(db, playlist, offset, locale = nil, continuation = nil) + # Show empy playlist if requested page is out of range + if offset >= playlist.video_count + return [] of PlaylistVideo + end + if playlist.is_a? InvidiousPlaylist - db.query_all("SELECT * FROM playlist_videos WHERE plid = $1 ORDER BY array_position($2, index) LIMIT 100 OFFSET $3", playlist.id, playlist.index, offset, as: PlaylistVideo) + db.query_all("SELECT * FROM playlist_videos WHERE plid = $1 ORDER BY array_position($2, index) LIMIT 100 OFFSET $3", + playlist.id, playlist.index, offset, as: PlaylistVideo) else - fetch_playlist_videos(playlist.id, playlist.video_count, offset, locale, continuation) + if offset >= 100 + # Normalize offset to match youtube's behavior (100 videos chunck per request) + offset = (offset / 100).to_i64 * 100_i64 + + ctoken = produce_playlist_continuation(playlist.id, offset) + initial_data = JSON.parse(request_youtube_api_browse(ctoken)).as_h + else + response = YT_POOL.client &.get("/playlist?list=#{playlist.id}&gl=US&hl=en") + initial_data = extract_initial_data(response.body) + end + + if initial_data + return extract_playlist_videos(initial_data) + else + return [] of PlaylistVideo + end end end -def fetch_playlist_videos(plid, video_count, offset = 0, locale = nil, continuation = nil) - if continuation - response = YT_POOL.client &.get("/watch?v=#{continuation}&list=#{plid}&gl=US&hl=en") - initial_data = extract_initial_data(response.body) - offset = initial_data["currentVideoEndpoint"]?.try &.["watchEndpoint"]?.try &.["index"]?.try &.as_i64 || offset - end - - response = YT_POOL.client &.get("/playlist?list=#{plid}&gl=US&hl=en") - initial_data = extract_initial_data(response.body) - - return [] of PlaylistVideo if !initial_data - videos = extract_playlist_videos(initial_data) - - until videos.empty? || videos[0].index == offset - videos.shift - end - - return videos -end - def extract_playlist_videos(initial_data : Hash(String, JSON::Any)) videos = [] of PlaylistVideo - (initial_data["contents"]?.try &.["twoColumnBrowseResultsRenderer"]["tabs"].as_a.select(&.["tabRenderer"]["selected"]?.try &.as_bool)[0]["tabRenderer"]["content"]["sectionListRenderer"]["contents"][0]["itemSectionRenderer"]["contents"][0]["playlistVideoListRenderer"]["contents"].as_a || - initial_data["response"]?.try &.["continuationContents"]["playlistVideoListContinuation"]["contents"].as_a).try &.each do |item| + if initial_data["contents"]? + tabs = initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"] + tabs_renderer = tabs.as_a.select(&.["tabRenderer"]["selected"]?.try &.as_bool)[0]["tabRenderer"] + + if tabs_renderer["contents"]? + # Initial playlist data + list_renderer = tabs_renderer.["contents"]["sectionListRenderer"]["contents"][0] + item_renderer = list_renderer.["itemSectionRenderer"]["contents"][0] + contents = item_renderer.["playlistVideoListRenderer"]["contents"].as_a + else + # Continuation data + contents = initial_data["onResponseReceivedActions"][0]? + .try &.["appendContinuationItemsAction"]["continuationItems"].as_a + end + else + contents = initial_data["response"]?.try &.["continuationContents"]["playlistVideoListContinuation"]["contents"].as_a + end + + contents.try &.each do |item| if i = item["playlistVideoRenderer"]? video_id = i["navigationEndpoint"]["watchEndpoint"]["videoId"].as_s plid = i["navigationEndpoint"]["watchEndpoint"]["playlistId"].as_s From 94ecd29e3520b91cbe16d8099ae6b94272364f05 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 20 Mar 2021 22:29:54 +0000 Subject: [PATCH 05/11] Make use of youtube API helper in src/invidious/channels.cr --- src/invidious/channels.cr | 45 +++++++++++++-------------------------- 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index b9808d98..dfb9e078 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -229,18 +229,18 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) page = 1 LOGGER.trace("fetch_channel: #{ucid} : Downloading channel videos page") - response = get_channel_videos_response(ucid, page, auto_generated: auto_generated) + response_body = get_channel_videos_response(ucid, page, auto_generated: auto_generated) videos = [] of SearchVideo begin - initial_data = JSON.parse(response.body) + initial_data = JSON.parse(response_body) raise InfoException.new("Could not extract channel JSON") if !initial_data LOGGER.trace("fetch_channel: #{ucid} : Extracting videos from channel videos page initial_data") videos = extract_videos(initial_data.as_h, author, ucid) rescue ex - if response.body.includes?("To continue with your YouTube experience, please fill out the form below.") || - response.body.includes?("https://www.google.com/sorry/index") + if response_body.includes?("To continue with your YouTube experience, please fill out the form below.") || + response_body.includes?("https://www.google.com/sorry/index") raise InfoException.new("Could not extract channel info. Instance is likely blocked.") end raise ex @@ -304,8 +304,8 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) ids = [] of String loop do - response = get_channel_videos_response(ucid, page, auto_generated: auto_generated) - initial_data = JSON.parse(response.body) + response_body = get_channel_videos_response(ucid, page, auto_generated: auto_generated) + initial_data = JSON.parse(response_body) raise InfoException.new("Could not extract channel JSON") if !initial_data videos = extract_videos(initial_data.as_h, author, ucid) @@ -447,6 +447,7 @@ def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, so return continuation end +# Used in bypass_captcha_job.cr def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false) continuation = produce_channel_videos_continuation(ucid, page, auto_generated, sort_by, v2) return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en" @@ -938,34 +939,18 @@ def get_about_info(ucid, locale) end def get_channel_videos_response(ucid, page = 1, auto_generated = nil, sort_by = "newest", youtubei_browse = true) - if youtubei_browse - continuation = produce_channel_videos_continuation(ucid, page, auto_generated: auto_generated, sort_by: sort_by, v2: true) - data = { - "context": { - "client": { - "clientName": "WEB", - "clientVersion": "2.20201021.03.00", - }, - }, - "continuation": continuation, - }.to_json - return YT_POOL.client &.post( - "/youtubei/v1/browse?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8", - headers: HTTP::Headers{"content-type" => "application/json"}, - body: data - ) - else - url = produce_channel_videos_url(ucid, page, auto_generated: auto_generated, sort_by: sort_by, v2: true) - return YT_POOL.client &.get(url) - end + continuation = produce_channel_videos_continuation(ucid, page, + auto_generated: auto_generated, sort_by: sort_by, v2: true) + + return request_youtube_api_browse(continuation) end def get_60_videos(ucid, author, page, auto_generated, sort_by = "newest") videos = [] of SearchVideo 2.times do |i| - response = get_channel_videos_response(ucid, page * 2 + (i - 1), auto_generated: auto_generated, sort_by: sort_by) - initial_data = JSON.parse(response.body) + response_json = get_channel_videos_response(ucid, page * 2 + (i - 1), auto_generated: auto_generated, sort_by: sort_by) + initial_data = JSON.parse(response_json) break if !initial_data videos.concat extract_videos(initial_data.as_h, author, ucid) end @@ -974,8 +959,8 @@ def get_60_videos(ucid, author, page, auto_generated, sort_by = "newest") end def get_latest_videos(ucid) - response = get_channel_videos_response(ucid) - initial_data = JSON.parse(response.body) + response_json = get_channel_videos_response(ucid) + initial_data = JSON.parse(response_json) return [] of SearchVideo if !initial_data author = initial_data["metadata"]?.try &.["channelMetadataRenderer"]?.try &.["title"]?.try &.as_s items = extract_videos(initial_data.as_h, author, ucid) From 9bdfb0a32b1d606d1e966ab07d7a05fe56f75643 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 20 Mar 2021 22:45:27 +0000 Subject: [PATCH 06/11] Playlist: Support edge case where 'content' in JSON may be erroneously plural --- src/invidious/playlists.cr | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index 1ef71a84..508dc760 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -471,9 +471,12 @@ def extract_playlist_videos(initial_data : Hash(String, JSON::Any)) tabs = initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"] tabs_renderer = tabs.as_a.select(&.["tabRenderer"]["selected"]?.try &.as_bool)[0]["tabRenderer"] - if tabs_renderer["contents"]? + # Watch out the two versions, with and without "s" + if tabs_renderer["contents"]? || tabs_renderer["content"]? # Initial playlist data - list_renderer = tabs_renderer.["contents"]["sectionListRenderer"]["contents"][0] + tabs_contents = tabs_renderer.["contents"]? || tabs_renderer.["content"] + + list_renderer = tabs_contents.["sectionListRenderer"]["contents"][0] item_renderer = list_renderer.["itemSectionRenderer"]["contents"][0] contents = item_renderer.["playlistVideoListRenderer"]["contents"].as_a else From a61735e29ab58be0a5b6f5be3eea2fcb113d27fa Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 20 Mar 2021 22:47:51 +0000 Subject: [PATCH 07/11] Print detailed error message when playlist can't be retrieved --- src/invidious/routes/playlists.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr index c5023c08..19e6541f 100644 --- a/src/invidious/routes/playlists.cr +++ b/src/invidious/routes/playlists.cr @@ -440,7 +440,7 @@ class Invidious::Routes::Playlists < Invidious::Routes::BaseRoute begin videos = get_playlist_videos(PG_DB, playlist, offset: (page - 1) * 100, locale: locale) rescue ex - videos = [] of PlaylistVideo + return error_template(500, "Error encountered while retrieving playlist videos.
#{ex.message}") end if playlist.author == user.try &.email From de6db4141fad74c2dffbf9afe12e2e108b66b1bb Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 21 Mar 2021 01:09:47 +0100 Subject: [PATCH 08/11] Fix produce_playlist_continuation checks in spec/helpers_spec.cr --- spec/helpers_spec.cr | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/spec/helpers_spec.cr b/spec/helpers_spec.cr index 073d2700..a4aaff9f 100644 --- a/spec/helpers_spec.cr +++ b/spec/helpers_spec.cr @@ -47,19 +47,13 @@ describe "Helper" do end end - describe "#produce_playlist_url" do - it "correctly produces url for requesting index `x` of a playlist" do - produce_playlist_url("UUCla9fZca4I7KagBtgRGnOw", 0).should eq("/browse_ajax?continuation=4qmFsgIqEhpWTFVVQ2xhOWZaY2E0STdLYWdCdGdSR25PdxoMZWdaUVZEcERRVUU9&gl=US&hl=en") + describe "#produce_playlist_continuation" do + it "correctly produces ctoken for requesting index `x` of a playlist" do + produce_playlist_continuation("UUCla9fZca4I7KagBtgRGnOw", 100).should eq("4qmFsgJNEhpWTFVVQ2xhOWZaY2E0STdLYWdCdGdSR25PdxoUQ0FGNkJsQlVPa05IVVElM0QlM0SaAhhVVUNsYTlmWmNhNEk3S2FnQnRnUkduT3c%3D") - produce_playlist_url("UCCla9fZca4I7KagBtgRGnOw", 0).should eq("/browse_ajax?continuation=4qmFsgIqEhpWTFVVQ2xhOWZaY2E0STdLYWdCdGdSR25PdxoMZWdaUVZEcERRVUU9&gl=US&hl=en") + produce_playlist_continuation("UCCla9fZca4I7KagBtgRGnOw", 200).should eq("4qmFsgJLEhpWTFVVQ2xhOWZaY2E0STdLYWdCdGdSR25PdxoSQ0FKNkIxQlVPa05OWjBJJTNEmgIYVVVDbGE5ZlpjYTRJN0thZ0J0Z1JHbk93") - produce_playlist_url("PLt5AfwLFPxWLNVixpe1w3fi6lE2OTq0ET", 0).should eq("/browse_ajax?continuation=4qmFsgI0EiRWTFBMdDVBZndMRlB4V0xOVml4cGUxdzNmaTZsRTJPVHEwRVQaDGVnWlFWRHBEUVVFPQ%3D%3D&gl=US&hl=en") - - produce_playlist_url("PLt5AfwLFPxWLNVixpe1w3fi6lE2OTq0ET", 10000).should eq("/browse_ajax?continuation=4qmFsgI0EiRWTFBMdDVBZndMRlB4V0xOVml4cGUxdzNmaTZsRTJPVHEwRVQaDGVnZFFWRHBEU2tKUA%3D%3D&gl=US&hl=en") - - produce_playlist_url("PL55713C70BA91BD6E", 0).should eq("/browse_ajax?continuation=4qmFsgIkEhRWTFBMNTU3MTNDNzBCQTkxQkQ2RRoMZWdaUVZEcERRVUU9&gl=US&hl=en") - - produce_playlist_url("PL55713C70BA91BD6E", 10000).should eq("/browse_ajax?continuation=4qmFsgIkEhRWTFBMNTU3MTNDNzBCQTkxQkQ2RRoMZWdkUVZEcERTa0pQ&gl=US&hl=en") + produce_playlist_continuation("PL55713C70BA91BD6E", 100).should eq("4qmFsgJBEhRWTFBMNTU3MTNDNzBCQTkxQkQ2RRoUQ0FGNkJsQlVPa05IVVElM0QlM0SaAhJQTDU1NzEzQzcwQkE5MUJENkU%3D") end end From aaefa386029c39cd8f7a052bbfd6a338178c47fb Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 21 Mar 2021 16:05:50 +0100 Subject: [PATCH 09/11] Make the linter happy --- src/invidious/helpers/youtube_api.cr | 6 +++--- src/invidious/playlists.cr | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/invidious/helpers/youtube_api.cr b/src/invidious/helpers/youtube_api.cr index 0ae80318..30413532 100644 --- a/src/invidious/helpers/youtube_api.cr +++ b/src/invidious/helpers/youtube_api.cr @@ -11,9 +11,9 @@ def request_youtube_api_browse(continuation) data = { "context": { "client": { - "hl": "en", - "gl": "US", - "clientName": "WEB", + "hl": "en", + "gl": "US", + "clientName": "WEB", "clientVersion": HARDCODED_CLIENT_VERS, }, }, diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index 508dc760..71f6a9b8 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -315,14 +315,14 @@ def produce_playlist_continuation(id, index) # Emulate a "request counter" increment, to make perfectly valid # ctokens, even if at the time of writing, it's ignored by youtube. - request_count = (index / 100).to_i64 || 1_i64 + request_count = (index / 100).to_i64 || 1_i64 data = {"1:varint" => index.to_i64} .try { |i| Protodec::Any.cast_json(i) } .try { |i| Protodec::Any.from_json(i) } .try { |i| Base64.urlsafe_encode(i, padding: false) } - data_wrapper = { "1:varint" => request_count, "15:string" => "PT:#{data}" } + data_wrapper = {"1:varint" => request_count, "15:string" => "PT:#{data}"} .try { |i| Protodec::Any.cast_json(i) } .try { |i| Protodec::Any.from_json(i) } .try { |i| Base64.urlsafe_encode(i) } @@ -330,8 +330,8 @@ def produce_playlist_continuation(id, index) object = { "80226972:embedded" => { - "2:string" => plid, - "3:string" => data_wrapper, + "2:string" => plid, + "3:string" => data_wrapper, "35:string" => id, }, } From 3e88b72316198de0f58e46eac0d6c8799732b9e6 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 22 Mar 2021 18:53:17 +0100 Subject: [PATCH 10/11] Remove useless parameter 'youtubei_browse' in get_channel_videos_response() --- src/invidious/channels.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index dfb9e078..9a129e1e 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -938,7 +938,7 @@ def get_about_info(ucid, locale) }) end -def get_channel_videos_response(ucid, page = 1, auto_generated = nil, sort_by = "newest", youtubei_browse = true) +def get_channel_videos_response(ucid, page = 1, auto_generated = nil, sort_by = "newest") continuation = produce_channel_videos_continuation(ucid, page, auto_generated: auto_generated, sort_by: sort_by, v2: true) From 23e5b6ba72c3c39df97c4fd21980997b9a30e303 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Tue, 23 Mar 2021 02:25:47 +0000 Subject: [PATCH 11/11] Remove extra 'next page' button at then end of a playlist --- src/invidious/routes/playlists.cr | 7 +++++++ src/invidious/views/playlist.ecr | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr index 19e6541f..73c14155 100644 --- a/src/invidious/routes/playlists.cr +++ b/src/invidious/routes/playlists.cr @@ -433,6 +433,13 @@ class Invidious::Routes::Playlists < Invidious::Routes::BaseRoute return error_template(500, ex) end + page_count = (playlist.video_count / 100).to_i + page_count = 1 if page_count == 0 + + if page > page_count + return env.redirect "/playlist?list=#{plid}&page=#{page_count}" + end + if playlist.privacy == PlaylistPrivacy::Private && playlist.author != user.try &.email return error_template(403, "This playlist is private.") end diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index bb721c3a..91156028 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -103,7 +103,7 @@
- <% if videos.size == 100 %> + <% if page_count != 1 && page < page_count %> <%= translate(locale, "Next page") %>