From ffa9e5dfab7a1d7ea84d88007107f9f40295c50a Mon Sep 17 00:00:00 2001 From: Andre Borie Date: Sun, 17 Jan 2021 01:44:31 +0000 Subject: [PATCH 001/420] Make migrations (mostly) idempotent. --- config/sql/annotations.sql | 4 ++-- config/sql/channel_videos.sql | 6 +++--- config/sql/channels.sql | 6 +++--- config/sql/nonces.sql | 6 +++--- config/sql/playlist_videos.sql | 4 ++-- config/sql/playlists.sql | 4 ++-- config/sql/session_ids.sql | 6 +++--- config/sql/users.sql | 6 +++--- config/sql/videos.sql | 6 +++--- docker/init-invidious-db.sh | 18 +++++++++--------- 10 files changed, 33 insertions(+), 33 deletions(-) diff --git a/config/sql/annotations.sql b/config/sql/annotations.sql index 4ea077e7..3705829d 100644 --- a/config/sql/annotations.sql +++ b/config/sql/annotations.sql @@ -2,11 +2,11 @@ -- DROP TABLE public.annotations; -CREATE TABLE public.annotations +CREATE TABLE IF NOT EXISTS public.annotations ( id text NOT NULL, annotations xml, CONSTRAINT annotations_id_key UNIQUE (id) ); -GRANT ALL ON TABLE public.annotations TO kemal; +GRANT ALL ON TABLE public.annotations TO current_user; diff --git a/config/sql/channel_videos.sql b/config/sql/channel_videos.sql index cec57cd4..cd4e0ffd 100644 --- a/config/sql/channel_videos.sql +++ b/config/sql/channel_videos.sql @@ -2,7 +2,7 @@ -- DROP TABLE public.channel_videos; -CREATE TABLE public.channel_videos +CREATE TABLE IF NOT EXISTS public.channel_videos ( id text NOT NULL, title text, @@ -17,13 +17,13 @@ CREATE TABLE public.channel_videos CONSTRAINT channel_videos_id_key UNIQUE (id) ); -GRANT ALL ON TABLE public.channel_videos TO kemal; +GRANT ALL ON TABLE public.channel_videos TO current_user; -- Index: public.channel_videos_ucid_idx -- DROP INDEX public.channel_videos_ucid_idx; -CREATE INDEX channel_videos_ucid_idx +CREATE INDEX IF NOT EXISTS channel_videos_ucid_idx ON public.channel_videos USING btree (ucid COLLATE pg_catalog."default"); diff --git a/config/sql/channels.sql b/config/sql/channels.sql index b5a29b8f..55772da6 100644 --- a/config/sql/channels.sql +++ b/config/sql/channels.sql @@ -2,7 +2,7 @@ -- DROP TABLE public.channels; -CREATE TABLE public.channels +CREATE TABLE IF NOT EXISTS public.channels ( id text NOT NULL, author text, @@ -12,13 +12,13 @@ CREATE TABLE public.channels CONSTRAINT channels_id_key UNIQUE (id) ); -GRANT ALL ON TABLE public.channels TO kemal; +GRANT ALL ON TABLE public.channels TO current_user; -- Index: public.channels_id_idx -- DROP INDEX public.channels_id_idx; -CREATE INDEX channels_id_idx +CREATE INDEX IF NOT EXISTS channels_id_idx ON public.channels USING btree (id COLLATE pg_catalog."default"); diff --git a/config/sql/nonces.sql b/config/sql/nonces.sql index 7b8ce9f2..644ac32a 100644 --- a/config/sql/nonces.sql +++ b/config/sql/nonces.sql @@ -2,20 +2,20 @@ -- DROP TABLE public.nonces; -CREATE TABLE public.nonces +CREATE TABLE IF NOT EXISTS public.nonces ( nonce text, expire timestamp with time zone, CONSTRAINT nonces_id_key UNIQUE (nonce) ); -GRANT ALL ON TABLE public.nonces TO kemal; +GRANT ALL ON TABLE public.nonces TO current_user; -- Index: public.nonces_nonce_idx -- DROP INDEX public.nonces_nonce_idx; -CREATE INDEX nonces_nonce_idx +CREATE INDEX IF NOT EXISTS nonces_nonce_idx ON public.nonces USING btree (nonce COLLATE pg_catalog."default"); diff --git a/config/sql/playlist_videos.sql b/config/sql/playlist_videos.sql index b2b8d5c4..eedccbad 100644 --- a/config/sql/playlist_videos.sql +++ b/config/sql/playlist_videos.sql @@ -2,7 +2,7 @@ -- DROP TABLE public.playlist_videos; -CREATE TABLE playlist_videos +CREATE TABLE IF NOT EXISTS playlist_videos ( title text, id text, @@ -16,4 +16,4 @@ CREATE TABLE playlist_videos PRIMARY KEY (index,plid) ); -GRANT ALL ON TABLE public.playlist_videos TO kemal; +GRANT ALL ON TABLE public.playlist_videos TO current_user; diff --git a/config/sql/playlists.sql b/config/sql/playlists.sql index 468496cb..83efce48 100644 --- a/config/sql/playlists.sql +++ b/config/sql/playlists.sql @@ -13,7 +13,7 @@ CREATE TYPE public.privacy AS ENUM -- DROP TABLE public.playlists; -CREATE TABLE public.playlists +CREATE TABLE IF NOT EXISTS public.playlists ( title text, id text primary key, @@ -26,4 +26,4 @@ CREATE TABLE public.playlists index int8[] ); -GRANT ALL ON public.playlists TO kemal; +GRANT ALL ON public.playlists TO current_user; diff --git a/config/sql/session_ids.sql b/config/sql/session_ids.sql index afbabb67..c493769a 100644 --- a/config/sql/session_ids.sql +++ b/config/sql/session_ids.sql @@ -2,7 +2,7 @@ -- DROP TABLE public.session_ids; -CREATE TABLE public.session_ids +CREATE TABLE IF NOT EXISTS public.session_ids ( id text NOT NULL, email text, @@ -10,13 +10,13 @@ CREATE TABLE public.session_ids CONSTRAINT session_ids_pkey PRIMARY KEY (id) ); -GRANT ALL ON TABLE public.session_ids TO kemal; +GRANT ALL ON TABLE public.session_ids TO current_user; -- Index: public.session_ids_id_idx -- DROP INDEX public.session_ids_id_idx; -CREATE INDEX session_ids_id_idx +CREATE INDEX IF NOT EXISTS session_ids_id_idx ON public.session_ids USING btree (id COLLATE pg_catalog."default"); diff --git a/config/sql/users.sql b/config/sql/users.sql index 0f2cdba2..ad002ec2 100644 --- a/config/sql/users.sql +++ b/config/sql/users.sql @@ -2,7 +2,7 @@ -- DROP TABLE public.users; -CREATE TABLE public.users +CREATE TABLE IF NOT EXISTS public.users ( updated timestamp with time zone, notifications text[], @@ -16,13 +16,13 @@ CREATE TABLE public.users CONSTRAINT users_email_key UNIQUE (email) ); -GRANT ALL ON TABLE public.users TO kemal; +GRANT ALL ON TABLE public.users TO current_user; -- Index: public.email_unique_idx -- DROP INDEX public.email_unique_idx; -CREATE UNIQUE INDEX email_unique_idx +CREATE UNIQUE INDEX IF NOT EXISTS email_unique_idx ON public.users USING btree (lower(email) COLLATE pg_catalog."default"); diff --git a/config/sql/videos.sql b/config/sql/videos.sql index 8def2f83..7040703c 100644 --- a/config/sql/videos.sql +++ b/config/sql/videos.sql @@ -2,7 +2,7 @@ -- DROP TABLE public.videos; -CREATE TABLE public.videos +CREATE TABLE IF NOT EXISTS public.videos ( id text NOT NULL, info text, @@ -10,13 +10,13 @@ CREATE TABLE public.videos CONSTRAINT videos_pkey PRIMARY KEY (id) ); -GRANT ALL ON TABLE public.videos TO kemal; +GRANT ALL ON TABLE public.videos TO current_user; -- Index: public.id_idx -- DROP INDEX public.id_idx; -CREATE UNIQUE INDEX id_idx +CREATE UNIQUE INDEX IF NOT EXISTS id_idx ON public.videos USING btree (id COLLATE pg_catalog."default"); diff --git a/docker/init-invidious-db.sh b/docker/init-invidious-db.sh index 3808e673..cc0e1c3f 100755 --- a/docker/init-invidious-db.sh +++ b/docker/init-invidious-db.sh @@ -5,12 +5,12 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E CREATE USER postgres; EOSQL -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/channels.sql -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/videos.sql -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/channel_videos.sql -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/users.sql -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/session_ids.sql -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/nonces.sql -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/annotations.sql -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/playlists.sql -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/playlist_videos.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/channels.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/videos.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/channel_videos.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/users.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/session_ids.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/nonces.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/annotations.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/playlists.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/playlist_videos.sql From 89fd35e02d0e8de585c3f5d619fb45986ac1173a Mon Sep 17 00:00:00 2001 From: Andrew Zhao Date: Thu, 18 Mar 2021 01:23:32 -0400 Subject: [PATCH 002/420] fix comment replies --- assets/js/handlers.js | 3 ++- assets/js/watch.js | 7 +++++-- src/invidious.cr | 5 ++++- src/invidious/comments.cr | 17 +++++++++++------ 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/assets/js/handlers.js b/assets/js/handlers.js index b3da8d9b..1498f39a 100644 --- a/assets/js/handlers.js +++ b/assets/js/handlers.js @@ -22,7 +22,8 @@ break; case 'get_youtube_replies': var load_more = e.getAttribute('data-load-more') !== null; - get_youtube_replies(e, load_more); + var load_replies = e.getAttribute('data-load-replies') !== null; + get_youtube_replies(e, load_more, load_replies); break; case 'toggle_parent': toggle_parent(e); diff --git a/assets/js/watch.js b/assets/js/watch.js index eb493bf3..3909edd4 100644 --- a/assets/js/watch.js +++ b/assets/js/watch.js @@ -359,7 +359,7 @@ function get_youtube_comments(retries) { xhr.send(); } -function get_youtube_replies(target, load_more) { +function get_youtube_replies(target, load_more, load_replies) { var continuation = target.getAttribute('data-continuation'); var body = target.parentNode.parentNode; @@ -371,7 +371,10 @@ function get_youtube_replies(target, load_more) { '?format=html' + '&hl=' + video_data.preferences.locale + '&thin_mode=' + video_data.preferences.thin_mode + - '&continuation=' + continuation; + '&continuation=' + continuation + if (load_replies) { + url += '&action=action_get_comment_replies'; + } var xhr = new XMLHttpRequest(); xhr.responseType = 'json'; xhr.timeout = 10000; diff --git a/src/invidious.cr b/src/invidious.cr index 89d99ecc..8d579f92 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -2054,6 +2054,9 @@ get "/api/v1/comments/:id" do |env| format = env.params.query["format"]? format ||= "json" + action = env.params.query["action"]? + action ||= "action_get_comments" + continuation = env.params.query["continuation"]? sort_by = env.params.query["sort_by"]?.try &.downcase @@ -2061,7 +2064,7 @@ get "/api/v1/comments/:id" do |env| sort_by ||= "top" begin - comments = fetch_youtube_comments(id, PG_DB, continuation, format, locale, thin_mode, region, sort_by: sort_by) + comments = fetch_youtube_comments(id, PG_DB, continuation, format, locale, thin_mode, region, sort_by: sort_by, action: action) rescue ex next error_json(500, ex) end diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 20e64a08..e7e87203 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -56,7 +56,7 @@ class RedditListing property modhash : String end -def fetch_youtube_comments(id, db, cursor, format, locale, thin_mode, region, sort_by = "top") +def fetch_youtube_comments(id, db, cursor, format, locale, thin_mode, region, sort_by = "top", action = "action_get_comments") video = get_video(id, db, region: region) session_token = video.session_token @@ -88,9 +88,14 @@ def fetch_youtube_comments(id, db, cursor, format, locale, thin_mode, region, so "cookie" => video.cookie, } - response = YT_POOL.client(region, &.post("/comment_service_ajax?action_get_comments=1&hl=en&gl=US&pbj=1", headers, form: post_req)) + response = YT_POOL.client(region, &.post("/comment_service_ajax?#{action}=1&hl=en&gl=US&pbj=1", headers, form: post_req)) response = JSON.parse(response.body) + # For some reason youtube puts it in an array for comment_replies but otherwise it's the same + if action == "action_get_comment_replies" + response = response[1] + end + if !response["response"]["continuationContents"]? raise InfoException.new("Could not fetch comments") end @@ -228,7 +233,7 @@ def fetch_youtube_comments(id, db, cursor, format, locale, thin_mode, region, so if format == "html" response = JSON.parse(response) - content_html = template_youtube_comments(response, locale, thin_mode) + content_html = template_youtube_comments(response, locale, thin_mode, action == "action_get_comment_replies") response = JSON.build do |json| json.object do @@ -281,7 +286,7 @@ def fetch_reddit_comments(id, sort_by = "confidence") return comments, thread end -def template_youtube_comments(comments, locale, thin_mode) +def template_youtube_comments(comments, locale, thin_mode, is_replies = false) String.build do |html| root = comments["comments"].as_a root.each do |child| @@ -292,7 +297,7 @@ def template_youtube_comments(comments, locale, thin_mode)

#{translate(locale, "View `x` replies", number_with_separator(child["replies"]["replyCount"]))} + data-onclick="get_youtube_replies" data-load-replies>#{translate(locale, "View `x` replies", number_with_separator(child["replies"]["replyCount"]))}

@@ -412,7 +417,7 @@ def template_youtube_comments(comments, locale, thin_mode)

#{translate(locale, "Load more")} + data-onclick="get_youtube_replies" data-load-more #{"data-load-replies" if is_replies}>#{translate(locale, "Load more")}

From fec82df4516c48e27ef12ed7e48faf7e9590d332 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 18 Mar 2021 23:11:46 +0000 Subject: [PATCH 003/420] 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 004/420] 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 005/420] 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 006/420] 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 007/420] 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 008/420] 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 009/420] 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 010/420] 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 011/420] 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 012/420] 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 013/420] 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") %> From 61d49a12150adfca91df98709488f0f29952f3d3 Mon Sep 17 00:00:00 2001 From: Andrew Zhao Date: Wed, 24 Mar 2021 00:06:13 -0400 Subject: [PATCH 014/420] remove comments extract cursor --- spec/helpers_spec.cr | 8 -------- src/invidious/comments.cr | 12 +----------- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/spec/helpers_spec.cr b/spec/helpers_spec.cr index a4aaff9f..c4138671 100644 --- a/spec/helpers_spec.cr +++ b/spec/helpers_spec.cr @@ -71,14 +71,6 @@ describe "Helper" do end end - describe "#extract_comment_cursor" do - it "correctly extracts a comment cursor from a given continuation" do - extract_comment_cursor("EiYSC2tKUVA3a2l3NUZrwAEByAEB4AEBogINKP___________wFAABgGMpwFCoYFQURTSl9pM1RqN1VlZ3dBd1daZkk4TmNiZ0djLVp0NFZEaW1BUGZwWHlPNDhuYUFxa3BsOXZYTk41OWpGXzNGRkVZeVpJOHRGWWpla0w1Z2ktcjhLdGFhcmduMDFxTUpsQ19QN2NaLWU5VGxxbTgzeUN6QVFHSUVtMGlMbUs5ZmVNOUVmNVo2S24xclpPRmlOdkxJS3JIUlJhWS10dkFNdzBDb0R3UWxiSXdpNDAzNkNCQ0ZXY2syemh1VHBsdEVUa2RmRHVrYVdkNnR1X1F4dkdnMGRkeEMydnNuVnlsQ1lJSUliWjAwMk1UTmpsbWJ5ejNKeGVybHJoa1drNW9kODZhOS16RVBPMjRHVzRKZnJlZEFvdGtzRmtCUUx5RWNRbkxRdHVyMHNwbGNmLUswZUttTlZkbk1DY1JVUF9LaU8tdVk4Qmg4RmtCa2RwMTFhVW10R0tzMWM0VjZXVkwwc29TallQc0VGLUF0LWlEVENJVXRNT1RLZklMblJ2V2NJclJvWndUNHA2MXFFMnhuN01CSFVJMzJJRjhJN2pKanh4a2o3ekMtUXBuT0xFdUNGOGJlN29kekFDa2VfTzVZNnpHM1FzN0lDM3NvV0NFbVJiLXlPNzB0ZDlXS3lXc25UNTJqM0FVT3hiQW16NU1EeU9qUVN3SERLNlFmaVh6N3ZjbGZnWEgxSUlqVmFCVUc3bkhlZkFOMlNoZ1BnN1hwaHBrV0FUdUtnRjNtRnBNRmViTFp2bHVPQ1k1WkgxVTh5LWV1ZnN5UUhxQkZJVlh0Mkg1NEFVa0xZeGdORmJTY0dfaEE4dEswV0JwdkdGUmE0V2dmT3NsNjlRSmRISTBKbWlOeS1rdyIPIgtrSlFQN2tpdzVGazAAKCg%3D").should eq("ADSJ_i3Tj7UegwAwWZfI8NcbgGc-Zt4VDimAPfpXyO48naAqkpl9vXNN59jF_3FFEYyZI8tFYjekL5gi-r8Ktaargn01qMJlC_P7cZ-e9Tlqm83yCzAQGIEm0iLmK9feM9Ef5Z6Kn1rZOFiNvLIKrHRRaY-tvAMw0CoDwQlbIwi4036CBCFWck2zhuTpltETkdfDukaWd6tu_QxvGg0ddxC2vsnVylCYIIIbZ002MTNjlmbyz3JxerlrhkWk5od86a9-zEPO24GW4JfredAotksFkBQLyEcQnLQtur0splcf-K0eKmNVdnMCcRUP_KiO-uY8Bh8FkBkdp11aUmtGKs1c4V6WVL0soSjYPsEF-At-iDTCIUtMOTKfILnRvWcIrRoZwT4p61qE2xn7MBHUI32IF8I7jJjxxkj7zC-QpnOLEuCF8be7odzACke_O5Y6zG3Qs7IC3soWCEmRb-yO70td9WKyWsnT52j3AUOxbAmz5MDyOjQSwHDK6QfiXz7vclfgXH1IIjVaBUG7nHefAN2ShgPg7XphpkWATuKgF3mFpMFebLZvluOCY5ZH1U8y-eufsyQHqBFIVXt2H54AUkLYxgNFbScG_hA8tK0WBpvGFRa4WgfOsl69QJdHI0JmiNy-kw") - - extract_comment_cursor("EiYSC2tKUVA3a2l3NUZrwAEByAEB4AEBogINKP___________wFAABgGMo4DCvgCQURTSl9pMEhLLWg2SGRybURYZV93VXA3b1VuVmhFZlJtcUNndUxPaEtTNnlONURSdTAxZ2RQUVBEQkw3ZFVJci1fNDRPc3dVUDF0WjE1YVczMUJjN1JNb2ZCdzc0cDhyVnFLcWVzUDFPZnhOXzhDRlV2ZHo0aDlvalM1UzFJbjEzVGVXQkx5TmxlcHhRSy00Ymhwd1I0Q3FIN2I1YlBvMkw2ZE8xdklXc3VsRmJQQXpQb29XTkhPdGlHdlRsbmFybEl2VFBPb3BzcTFsd3RUanhSZ25yU0d2SlhscHFPeUpZb0tyR01Cam5nREk2ZFMxcTU2UEt1ajlvbTc4WTFvckhiZzhaOEZrNG54NUFDd2lCSjYtLTBoOXhpNnpSMi1oeTRnTTlGWnFIeHU1QlgwQzBCczJ0WEJ4V1BoTWVPVUtPVjh6UVFaOTNXdTlhc284THdPMVVJZmtkdWgxSTVMY0NaWUlPLXd1c1UxcnN5MWV5ekQtZ0NBTiIPIgtrSlFQN2tpdzVGazAAKCg%3D").should eq("ADSJ_i0HK-h6HdrmDXe_wUp7oUnVhEfRmqCguLOhKS6yN5DRu01gdPQPDBL7dUIr-_44OswUP1tZ15aW31Bc7RMofBw74p8rVqKqesP1OfxN_8CFUvdz4h9ojS5S1In13TeWBLyNlepxQK-4bhpwR4CqH7b5bPo2L6dO1vIWsulFbPAzPooWNHOtiGvTlnarlIvTPOopsq1lwtTjxRgnrSGvJXlpqOyJYoKrGMBjngDI6dS1q56PKuj9om78Y1orHbg8Z8Fk4nx5ACwiBJ6--0h9xi6zR2-hy4gM9FZqHxu5BX0C0Bs2tXBxWPhMeOUKOV8zQQZ93Wu9aso8LwO1UIfkduh1I5LcCZYIO-wusU1rsy1eyzD-gCAN") - end - end - describe "#produce_comment_continuation" do it "correctly produces a continuation token for comments" do produce_comment_continuation("_cE8xSu6swE", "ADSJ_i2qvJeFtL0htmS5_K5Ctj3eGFVBMWL9Wd42o3kmUL6_mAzdLp85-liQZL0mYr_16BhaggUqX652Sv9JqV6VXinShSP-ZT6rL4NolPBaPXVtJsO5_rA_qE3GubAuLFw9uzIIXU2-HnpXbdgPLWTFavfX206hqWmmpHwUOrmxQV_OX6tYkM3ux3rPAKCDrT8eWL7MU3bLiNcnbgkW8o0h8KYLL_8BPa8LcHbTv8pAoNkjerlX1x7K4pqxaXPoyz89qNlnh6rRx6AXgAzzoHH1dmcyQ8CIBeOHg-m4i8ZxdX4dP88XWrIFg-jJGhpGP8JUMDgZgavxVx225hUEYZMyrLGler5em4FgbG62YWC51moLDLeYEA").should eq("EkMSC19jRTh4U3U2c3dFyAEA4AEBogINKP___________wFAAMICHQgEGhdodHRwczovL3d3dy55b3V0dWJlLmNvbSIAGAYyjAMK9gJBRFNKX2kycXZKZUZ0TDBodG1TNV9LNUN0ajNlR0ZWQk1XTDlXZDQybzNrbVVMNl9tQXpkTHA4NS1saVFaTDBtWXJfMTZCaGFnZ1VxWDY1MlN2OUpxVjZWWGluU2hTUC1aVDZyTDROb2xQQmFQWFZ0SnNPNV9yQV9xRTNHdWJBdUxGdzl1eklJWFUyLUhucFhiZGdQTFdURmF2ZlgyMDZocVdtbXBId1VPcm14UVZfT1g2dFlrTTN1eDNyUEFLQ0RyVDhlV0w3TVUzYkxpTmNuYmdrVzhvMGg4S1lMTF84QlBhOExjSGJUdjhwQW9Oa2plcmxYMXg3SzRwcXhhWFBveXo4OXFObG5oNnJSeDZBWGdBenpvSEgxZG1jeVE4Q0lCZU9IZy1tNGk4WnhkWDRkUDg4WFdySUZnLWpKR2hwR1A4SlVNRGdaZ2F2eFZ4MjI1aFVFWVpNeXJMR2xlcjVlbTRGZ2JHNjJZV0M1MW1vTERMZVlFQSIPIgtfY0U4eFN1NnN3RTAAKBQ%3D") diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index e7e87203..5d72503e 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -226,7 +226,7 @@ def fetch_youtube_comments(id, db, cursor, format, locale, thin_mode, region, so if body["continuations"]? continuation = body["continuations"][0]["nextContinuationData"]["continuation"].as_s - json.field "continuation", cursor.try &.starts_with?("E") ? continuation : extract_comment_cursor(continuation) + json.field "continuation", continuation end end end @@ -580,16 +580,6 @@ def content_to_comment_html(content) return comment_html end -def extract_comment_cursor(continuation) - cursor = URI.decode_www_form(continuation) - .try { |i| Base64.decode(i) } - .try { |i| IO::Memory.new(i) } - .try { |i| Protodec::Any.parse(i) } - .try { |i| i["6:2:embedded"]["1:0:string"].as_s } - - return cursor -end - def produce_comment_continuation(video_id, cursor = "", sort_by = "top") object = { "2:embedded" => { From e49aaa021646476b1499f7f612c13d5ba780462f Mon Sep 17 00:00:00 2001 From: Svallinn <41585298+Svallinn@users.noreply.github.com> Date: Wed, 24 Mar 2021 05:15:06 +0000 Subject: [PATCH 015/420] Fix channel search API --- src/invidious/search.cr | 46 ++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/src/invidious/search.cr b/src/invidious/search.cr index cf8fd790..26161fab 100644 --- a/src/invidious/search.cr +++ b/src/invidious/search.cr @@ -231,20 +231,27 @@ end alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist def channel_search(query, page, channel) - response = YT_POOL.client &.get("/channel/#{channel}?hl=en&gl=US") - response = YT_POOL.client &.get("/user/#{channel}?hl=en&gl=US") if response.headers["location"]? - response = YT_POOL.client &.get("/c/#{channel}?hl=en&gl=US") if response.headers["location"]? + response = YT_POOL.client &.get("/channel/#{channel}") - ucid = response.body.match(/\\"channelId\\":\\"(?[^\\]+)\\"/).try &.["ucid"]? + if response.status_code == 404 + response = YT_POOL.client &.get("/user/#{channel}") + response = YT_POOL.client &.get("/c/#{channel}") if response.status_code == 404 + ucid = response.body.match(/HeaderRenderer":\{"channelId":"(?[^\\"]+)"/).try &.["ucid"]? + else + ucid = channel + end - return 0, [] of SearchItem if !ucid + continuation = produce_channel_search_continuation(ucid, query, page) + response_json = request_youtube_api_browse(continuation) - url = produce_channel_search_url(ucid, query, page) - response = YT_POOL.client &.get(url) - initial_data = JSON.parse(response.body).as_a.find &.["response"]? - return 0, [] of SearchItem if !initial_data - author = initial_data["response"]?.try &.["metadata"]?.try &.["channelMetadataRenderer"]?.try &.["title"]?.try &.as_s - items = extract_items(initial_data.as_h, author, ucid) + result = JSON.parse(response_json.match(/"continuationItems": (?\[.*\]),/m).try &.["items"] || "{}") + return 0, [] of SearchItem if result.size == 0 + + items = [] of SearchItem + result.as_a.select(&.as_h.has_key?("itemSectionRenderer")).each { |item| + extract_item(item["itemSectionRenderer"]["contents"].as_a[0]) + .try { |t| items << t } + } return items.size, items end @@ -361,17 +368,28 @@ def produce_search_params(page = 1, sort : String = "relevance", date : String = return params end -def produce_channel_search_url(ucid, query, page) +def produce_channel_search_continuation(ucid, query, page) + if page <= 1 + idx = 0_i64 + else + idx = 30_i64 * (page - 1) + end + object = { "80226972:embedded" => { "2:string" => ucid, "3:base64" => { "2:string" => "search", + "6:varint" => 1_i64, "7:varint" => 1_i64, - "15:string" => "#{page}", + "12:varint" => 1_i64, + "15:base64" => { + "3:varint" => idx, + }, "23:varint" => 0_i64, }, "11:string" => query, + "35:string" => "browse-feed#{ucid}search", }, } @@ -380,7 +398,7 @@ def produce_channel_search_url(ucid, query, page) .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 process_search_query(query, page, user, region) From d652ab9920354517a122a17a5ba241fab5651c1a Mon Sep 17 00:00:00 2001 From: Svallinn <41585298+Svallinn@users.noreply.github.com> Date: Wed, 24 Mar 2021 05:15:30 +0000 Subject: [PATCH 016/420] Modify spec file --- spec/helpers_spec.cr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/helpers_spec.cr b/spec/helpers_spec.cr index a4aaff9f..06a772b5 100644 --- a/spec/helpers_spec.cr +++ b/spec/helpers_spec.cr @@ -27,11 +27,11 @@ describe "Helper" do end end - describe "#produce_channel_search_url" do + describe "#produce_channel_search_continuation" do it "correctly produces token for searching a specific channel" do - produce_channel_search_url("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100).should eq("/browse_ajax?continuation=4qmFsgI2EhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaGEVnWnpaV0Z5WTJnNEFYb0RNVEF3dUFFQVoA&gl=US&hl=en") + produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100).should eq("4qmFsgJqEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWnpaV0Z5WTJnd0FUZ0JZQUY2QkVkS2IxaTRBUUE9WgCaAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D") - produce_channel_search_url("UCXuqSBlHAE6Xw-yeJA0Tunw", "По ожиशुपतिरपि子而時ஸ்றீனி", 0).should eq("/browse_ajax?continuation=4qmFsgJ0EhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaGEVnWnpaV0Z5WTJnNEFYb0JNTGdCQUE9PVo-0J_QviDQvtC20LjgpLbgpYHgpKrgpKTgpL_gpLDgpKrgpL_lrZDogIzmmYLgrrjgr43grrHgr4Dgrqngrr8%3D&gl=US&hl=en") + produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "По ожиशुपतिरपि子而時ஸ்றீனி", 0).should eq("4qmFsgKoARIYVUNYdXFTQmxIQUU2WHcteWVKQTBUdW53GiBFZ1p6WldGeVkyZ3dBVGdCWUFGNkJFZEJRVDI0QVFBPVo-0J_QviDQvtC20LjgpLbgpYHgpKrgpKTgpL_gpLDgpKrgpL_lrZDogIzmmYLgrrjgr43grrHgr4Dgrqngrr-aAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D") end end From cbdba66ef3f770c31a99a16a47925ed37439b543 Mon Sep 17 00:00:00 2001 From: Svallinn <41585298+Svallinn@users.noreply.github.com> Date: Wed, 24 Mar 2021 05:33:45 +0000 Subject: [PATCH 017/420] Use the youtubei API over the legacy one --- src/invidious.cr | 6 +++--- src/invidious/channels.cr | 26 +++++++++++++++----------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 8d579f92..88b9ad85 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -1699,7 +1699,7 @@ get "/channel/:ucid" do |env| sort_options = {"last", "oldest", "newest"} sort_by ||= "last" - items, continuation = fetch_channel_playlists(channel.ucid, channel.author, channel.auto_generated, continuation, sort_by) + items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by) items.uniq! do |item| if item.responds_to?(:title) item.title @@ -1766,7 +1766,7 @@ get "/channel/:ucid/playlists" do |env| next env.redirect "/channel/#{channel.ucid}" end - items, continuation = fetch_channel_playlists(channel.ucid, channel.author, channel.auto_generated, continuation, sort_by) + items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by) items = items.select { |item| item.is_a?(SearchPlaylist) }.map { |item| item.as(SearchPlaylist) } items.each { |item| item.author = "" } @@ -2467,7 +2467,7 @@ end next error_json(500, ex) end - items, continuation = fetch_channel_playlists(channel.ucid, channel.author, channel.auto_generated, continuation, sort_by) + items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by) JSON.build do |json| json.object do diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index 9a129e1e..f7aa99e2 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -355,14 +355,19 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) return channel end -def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by) - if continuation || auto_generated - url = produce_channel_playlists_url(ucid, continuation, sort_by, auto_generated) +def fetch_channel_playlists(ucid, author, continuation, sort_by) + if continuation + response_json = request_youtube_api_browse(continuation) + result = JSON.parse(response_json.match(/"continuationItems": (?\[.*\]),/m).try &.["items"] || "{}") - response = YT_POOL.client &.get(url) + return [] of SearchItem, nil if result.size == 0 - continuation = response.body.match(/"continuation":"(?[^"]+)"/).try &.["continuation"]? - initial_data = JSON.parse(response.body).as_a.find(&.["response"]?).try &.as_h + items = [] of SearchItem + result.as_a.select(&.as_h.has_key?("gridPlaylistRenderer")).each { |item| + extract_item(item, author, ucid).try { |t| items << t } + } + + continuation = result.as_a.last["continuationItemRenderer"]?.try &.["continuationEndpoint"]["continuationCommand"]["token"].as_s else url = "/channel/#{ucid}/playlists?flow=list&view=1" @@ -377,13 +382,12 @@ def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by) end response = YT_POOL.client &.get(url) - continuation = response.body.match(/"continuation":"(?[^"]+)"/).try &.["continuation"]? initial_data = extract_initial_data(response.body) - end + return [] of SearchItem, nil if !initial_data - return [] of SearchItem, nil if !initial_data - items = extract_items(initial_data) - continuation = extract_channel_playlists_cursor(continuation, auto_generated) if continuation + items = extract_items(initial_data, author, ucid) + continuation = response.body.match(/"token":"(?[^"]+)"/).try &.["continuation"]? + end return items, continuation end From aa4c623a06ee6c1bed864a92322876d1503a6857 Mon Sep 17 00:00:00 2001 From: Svallinn <41585298+Svallinn@users.noreply.github.com> Date: Wed, 24 Mar 2021 05:34:23 +0000 Subject: [PATCH 018/420] Add deprecation note --- src/invidious/channels.cr | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index f7aa99e2..3a9b8641 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -457,6 +457,15 @@ def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = " return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en" end +# ## NOTE: DEPRECATED +# Reason -> Unstable +# The Protobuf object must be provided with an id of the last playlist from the current "page" +# in order to fetch the next one accurately +# (if the id isn't included, entries shift around erratically between pages, +# leading to repetitions and skip overs) +# +# Since it's impossible to produce the appropriate Protobuf without an id being provided by the user, +# it's better to stick to continuation tokens provided by the first request and onward def produce_channel_playlists_url(ucid, cursor, sort = "newest", auto_generated = false) object = { "80226972:embedded" => { From e248e7ebaf2ec32d790f7e88a96421cc2d418c9d Mon Sep 17 00:00:00 2001 From: Svallinn <41585298+Svallinn@users.noreply.github.com> Date: Wed, 24 Mar 2021 05:35:26 +0000 Subject: [PATCH 019/420] Remove unused function and related test --- spec/helpers_spec.cr | 6 ------ src/invidious/channels.cr | 25 ------------------------- 2 files changed, 31 deletions(-) diff --git a/spec/helpers_spec.cr b/spec/helpers_spec.cr index a4aaff9f..a58c1e5a 100644 --- a/spec/helpers_spec.cr +++ b/spec/helpers_spec.cr @@ -41,12 +41,6 @@ describe "Helper" do end end - describe "#extract_channel_playlists_cursor" do - it "correctly extracts a playlists cursor from the given URL" do - extract_channel_playlists_cursor("4qmFsgLRARIYVUNDajk1NklGNjJGYlQ3R291c3phajl3GrQBRWdsd2JHRjViR2x6ZEhNWUF5QUJNQUk0QVdBQmFnQjZabEZWYkZCaE1XczFVbFpHZDJGV09XNWxWelI0V0RGR2VWSnVWbUZOV0Vwc1ZHcG5lRmd3TVU1aVZXdDRWMWN4YzFGdFNuTmtlbWh4VGpCd1NWTllVa1pTYTJNeFlVUmtlRmt3Y0ZWVWJWRXdWbnBzTkU1V1JqRmhNVGxFVm14dmQwMXFhRzVXZDdnQkFBJTNEJTNE", false).should eq("AIOkY9EQpi_gyn1_QrFuZ1reN81_MMmI1YmlBblw8j7JHItEFG5h7qcJTNd4W9x5Quk_CVZ028gW") - end - end - 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") diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index 3a9b8641..30138d82 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -512,31 +512,6 @@ def produce_channel_playlists_url(ucid, cursor, sort = "newest", auto_generated return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en" end -def extract_channel_playlists_cursor(cursor, auto_generated) - cursor = URI.decode_www_form(cursor) - .try { |i| Base64.decode(i) } - .try { |i| IO::Memory.new(i) } - .try { |i| Protodec::Any.parse(i) } - .try { |i| i["80226972:0:embedded"]["3:1:base64"].as_h.find { |k, v| k.starts_with? "15:" } } - .try &.[1] - - if cursor.try &.as_h? - cursor = cursor.try { |i| Protodec::Any.cast_json(i.as_h) } - .try { |i| Protodec::Any.from_json(i) } - .try { |i| Base64.urlsafe_encode(i) } - .try { |i| URI.encode_www_form(i) } || "" - else - cursor = cursor.try &.as_s || "" - end - - if !auto_generated - cursor = URI.decode_www_form(cursor) - .try { |i| Base64.decode_string(i) } - end - - return cursor -end - # TODO: Add "sort_by" def fetch_channel_community(ucid, continuation, locale, format, thin_mode) response = YT_POOL.client &.get("/channel/#{ucid}/community?gl=US&hl=en") From f422a77014e5f2dced30534607d86f74da901178 Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 24 Mar 2021 18:07:18 -0700 Subject: [PATCH 020/420] Add translation to Audio Mode icon on vid result --- src/invidious/views/components/item.ecr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index ea7d356c..57df8347 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -141,7 +141,7 @@ <%= item.author %> - + " href="/watch?v=<%= item.id %>&listen=1">

From 56fab9d17847d258dd52ce278002953b3905b7c3 Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 24 Mar 2021 18:34:06 -0700 Subject: [PATCH 021/420] Add watch on youtube button on each video item --- src/invidious/views/components/item.ecr | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index 57df8347..3ff83f4a 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -141,6 +141,9 @@ <%= item.author %> + " href="https://youtube.com/watch?v=<%= item.id %>" style="margin-right: 5px;"> + + " href="/watch?v=<%= item.id %>&listen=1"> From 148071a74466f2244ca79bb35dbb7ea903afccca Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 25 Mar 2021 11:24:02 -0700 Subject: [PATCH 022/420] Add 'www' to URL on watch on youtube button --- src/invidious/views/components/item.ecr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index 3ff83f4a..9dfa047e 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -141,7 +141,7 @@ <%= item.author %> - " href="https://youtube.com/watch?v=<%= item.id %>" style="margin-right: 5px;"> + " href="https://www.youtube.com/watch?v=<%= item.id %>" style="margin-right: 5px;"> " href="/watch?v=<%= item.id %>&listen=1"> From c5ccefe6f7c127625c821d3b6836c23962a2b5b0 Mon Sep 17 00:00:00 2001 From: Svallinn <41585298+Svallinn@users.noreply.github.com> Date: Fri, 26 Mar 2021 03:52:28 +0000 Subject: [PATCH 023/420] Parse response to JSON instead of using regex --- src/invidious/channels.cr | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index 30138d82..2d5b4475 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -358,16 +358,20 @@ end def fetch_channel_playlists(ucid, author, continuation, sort_by) if continuation response_json = request_youtube_api_browse(continuation) - result = JSON.parse(response_json.match(/"continuationItems": (?\[.*\]),/m).try &.["items"] || "{}") + # result = JSON.parse(response_json.match(/"continuationItems": (?\[.*\]),/m).try &.["items"] || "{}") + result = JSON.parse(response_json) + continuationItems = result["onResponseReceivedActions"]? + .try &.[0]["appendContinuationItemsAction"]["continuationItems"] - return [] of SearchItem, nil if result.size == 0 + return [] of SearchItem, nil if !continuationItems items = [] of SearchItem - result.as_a.select(&.as_h.has_key?("gridPlaylistRenderer")).each { |item| + continuationItems.as_a.select(&.as_h.has_key?("gridPlaylistRenderer")).each { |item| extract_item(item, author, ucid).try { |t| items << t } } - continuation = result.as_a.last["continuationItemRenderer"]?.try &.["continuationEndpoint"]["continuationCommand"]["token"].as_s + continuation = continuationItems.as_a.last["continuationItemRenderer"]? + .try &.["continuationEndpoint"]["continuationCommand"]["token"].as_s else url = "/channel/#{ucid}/playlists?flow=list&view=1" From 8823753b4680e5e0589f3be4d1d2b0cbc4344dc9 Mon Sep 17 00:00:00 2001 From: Svallinn <41585298+Svallinn@users.noreply.github.com> Date: Fri, 26 Mar 2021 03:54:10 +0000 Subject: [PATCH 024/420] Remove commented line --- src/invidious/channels.cr | 1 - 1 file changed, 1 deletion(-) diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index 2d5b4475..47dfcbd6 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -358,7 +358,6 @@ end def fetch_channel_playlists(ucid, author, continuation, sort_by) if continuation response_json = request_youtube_api_browse(continuation) - # result = JSON.parse(response_json.match(/"continuationItems": (?\[.*\]),/m).try &.["items"] || "{}") result = JSON.parse(response_json) continuationItems = result["onResponseReceivedActions"]? .try &.[0]["appendContinuationItemsAction"]["continuationItems"] From acfa9e8a5525bea7f87429aa51e84cacd2ad641d Mon Sep 17 00:00:00 2001 From: Svallinn <41585298+Svallinn@users.noreply.github.com> Date: Fri, 26 Mar 2021 04:16:50 +0000 Subject: [PATCH 025/420] Parse responses to JSON instead of using regex --- src/invidious/search.cr | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/invidious/search.cr b/src/invidious/search.cr index 26161fab..4b216613 100644 --- a/src/invidious/search.cr +++ b/src/invidious/search.cr @@ -236,7 +236,9 @@ def channel_search(query, page, channel) if response.status_code == 404 response = YT_POOL.client &.get("/user/#{channel}") response = YT_POOL.client &.get("/c/#{channel}") if response.status_code == 404 - ucid = response.body.match(/HeaderRenderer":\{"channelId":"(?[^\\"]+)"/).try &.["ucid"]? + initial_data = extract_initial_data(response.body) + ucid = initial_data["header"]["c4TabbedHeaderRenderer"]?.try &.["channelId"].as_s? + raise InfoException.new("Impossible to extract channel ID from page") if !ucid else ucid = channel end @@ -244,11 +246,14 @@ def channel_search(query, page, channel) continuation = produce_channel_search_continuation(ucid, query, page) response_json = request_youtube_api_browse(continuation) - result = JSON.parse(response_json.match(/"continuationItems": (?\[.*\]),/m).try &.["items"] || "{}") - return 0, [] of SearchItem if result.size == 0 + result = JSON.parse(response_json) + continuationItems = result["onResponseReceivedActions"]? + .try &.[0]["appendContinuationItemsAction"]["continuationItems"] + + return 0, [] of SearchItem if !continuationItems items = [] of SearchItem - result.as_a.select(&.as_h.has_key?("itemSectionRenderer")).each { |item| + continuationItems.as_a.select(&.as_h.has_key?("itemSectionRenderer")).each { |item| extract_item(item["itemSectionRenderer"]["contents"].as_a[0]) .try { |t| items << t } } From b3099001bec5927dcb4634a83843c177fadc95a6 Mon Sep 17 00:00:00 2001 From: syeopite Date: Fri, 26 Mar 2021 13:51:22 -0700 Subject: [PATCH 026/420] Fix minor scaling issue in filter drop down. Basically prevents filter content from jumping above the dropbox when there's enough space to do --- assets/css/default.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/css/default.css b/assets/css/default.css index a76ecd48..2552263d 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -641,7 +641,7 @@ body.dark-theme { } #filters > summary { - display: inline-block; + display: block; margin-bottom: 15px; } From a7624d4724e01c68c8d53ac7cc38f7d7b38b273a Mon Sep 17 00:00:00 2001 From: syeopite Date: Sat, 27 Mar 2021 22:48:43 -0700 Subject: [PATCH 027/420] Fix trending API --- src/invidious/trending.cr | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/invidious/trending.cr b/src/invidious/trending.cr index 8d078387..910a99d8 100644 --- a/src/invidious/trending.cr +++ b/src/invidious/trending.cr @@ -6,24 +6,22 @@ def fetch_trending(trending_type, region, locale) plid = nil if trending_type && trending_type != "Default" - trending_type = trending_type.downcase.capitalize + if trending_type == "Music" + trending_type = 1 + elsif trending_type == "Gaming" + trending_type = 2 + elsif trending_type == "Movies" + trending_type = 3 + end response = YT_POOL.client &.get("/feed/trending?gl=#{region}&hl=en").body initial_data = extract_initial_data(response) + url = initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"][trending_type]["tabRenderer"]["endpoint"]["commandMetadata"]["webCommandMetadata"]["url"] + url = "#{url}&gl=#{region}&hl=en" - tabs = initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"][0]["tabRenderer"]["content"]["sectionListRenderer"]["subMenu"]["channelListSubMenuRenderer"]["contents"].as_a - url = tabs.select { |tab| tab["channelListSubMenuAvatarRenderer"]["title"]["simpleText"] == trending_type }[0]? - - if url - url["channelListSubMenuAvatarRenderer"]["navigationEndpoint"]["commandMetadata"]["webCommandMetadata"]["url"] - url = url["channelListSubMenuAvatarRenderer"]["navigationEndpoint"]["commandMetadata"]["webCommandMetadata"]["url"].as_s - url = "#{url}&gl=#{region}&hl=en" - trending = YT_POOL.client &.get(url).body - plid = extract_plid(url) - else - trending = YT_POOL.client &.get("/feed/trending?gl=#{region}&hl=en").body - end + trending = YT_POOL.client &.get(url).body + plid = extract_plid(url) else trending = YT_POOL.client &.get("/feed/trending?gl=#{region}&hl=en").body end From 8b75590d3efb25d53194c49a8768bcdaae7cf05f Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 28 Mar 2021 01:25:04 -0700 Subject: [PATCH 028/420] Remove news trending section from ui --- src/invidious/views/trending.ecr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/views/trending.ecr b/src/invidious/views/trending.ecr index 42acb15c..3ec62555 100644 --- a/src/invidious/views/trending.ecr +++ b/src/invidious/views/trending.ecr @@ -21,7 +21,7 @@
- <% {"Default", "Music", "Gaming", "News", "Movies"}.each do |option| %> + <% {"Default", "Music", "Gaming", "Movies"}.each do |option| %>
<% if trending_type == option %> <%= translate(locale, option) %> From b2f67cb15482572d95e3d411cfc0af4b0923abbb Mon Sep 17 00:00:00 2001 From: bongo bongo Date: Fri, 19 Mar 2021 20:39:35 +0000 Subject: [PATCH 029/420] Update Serbian (cyrillic) translation --- locales/sr_Cyrl.json | 98 ++++++++++++++++++++++---------------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/locales/sr_Cyrl.json b/locales/sr_Cyrl.json index 0ca9a8a0..adb25544 100644 --- a/locales/sr_Cyrl.json +++ b/locales/sr_Cyrl.json @@ -105,61 +105,61 @@ "Default homepage: ": "Подразумевана главна страница: ", "Feed menu: ": "Мени довода: ", "Top enabled: ": "", - "CAPTCHA enabled: ": "", - "Login enabled: ": "", - "Registration enabled: ": "", + "CAPTCHA enabled: ": "CAPTCHA укључена?: ", + "Login enabled: ": "Пријава укључена?: ", + "Registration enabled: ": "Регистрација укључена?: ", "Report statistics: ": "", - "Save preferences": "", - "Subscription manager": "", - "Token manager": "", - "Token": "", - "`x` subscriptions.": "", - "`x` tokens.": "", - "Import/export": "", - "unsubscribe": "", - "revoke": "", - "Subscriptions": "", - "`x` unseen notifications.": "", - "search": "", - "Log out": "", - "Released under the AGPLv3 by Omar Roth.": "", - "Source available here.": "", - "View JavaScript license information.": "", - "View privacy policy.": "", - "Trending": "", - "Public": "", - "Unlisted": "", - "Private": "", - "View all playlists": "", - "Updated `x` ago": "", - "Delete playlist `x`?": "", - "Delete playlist": "", - "Create playlist": "", - "Title": "", - "Playlist privacy": "", - "Editing playlist `x`": "", - "Watch on YouTube": "", - "Hide annotations": "", - "Show annotations": "", - "Genre: ": "", - "License: ": "", + "Save preferences": "Сачувај подешавања", + "Subscription manager": "Управљање праћењима", + "Token manager": "Управљање токенима", + "Token": "Токен", + "`x` subscriptions.": "`x`праћења.", + "`x` tokens.": "`x`токена.", + "Import/export": "Увези/извези", + "unsubscribe": "укини праћење", + "revoke": "опозови", + "Subscriptions": "Праћења", + "`x` unseen notifications.": "`x` непрочитаних обавештења.", + "search": "претрага", + "Log out": "Одјавите се", + "Released under the AGPLv3 by Omar Roth.": "Издао Омар Рот (Omar Roth) под условима AGPLv3 лиценце.", + "Source available here.": "Изворни код доступан овде.", + "View JavaScript license information.": "Прикажи информације о JavaScript лиценци.", + "View privacy policy.": "Прикажи извештај о приватности.", + "Trending": "У тренду", + "Public": "Јавно", + "Unlisted": "По позиву", + "Private": "Приватно", + "View all playlists": "Прикажи све плејлисте", + "Updated `x` ago": "Ажурирано пре `x`", + "Delete playlist `x`?": "Избриши плејлисту `x`?", + "Delete playlist": "Избриши плејлисту", + "Create playlist": "Направи плејлисту", + "Title": "Наслов", + "Playlist privacy": "Видљивост плејлисте", + "Editing playlist `x`": "Уређујете плејлисту `x`", + "Watch on YouTube": "Гледајте на YouTube-у", + "Hide annotations": "Сакриј анотације", + "Show annotations": "Прикажи анотације", + "Genre: ": "Жанр: ", + "License: ": "Лиценца: ", "Family friendly? ": "", "Wilson score: ": "", - "Engagement: ": "", - "Whitelisted regions: ": "", - "Blacklisted regions: ": "", + "Engagement: ": "Ангажовање: ", + "Whitelisted regions: ": "Дозвољене области: ", + "Blacklisted regions: ": "Забрањене области: ", "Shared `x`": "", - "`x` views.": "", - "Premieres in `x`": "", + "`x` views.": "`x` прегледа.", + "Premieres in `x`": "Емитује се уживо за `x`", "Premieres `x`": "", - "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "", - "View YouTube comments": "", - "View more comments on Reddit": "", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Здраво! Изгледа да је искључен JavaScript. Кликните овде да бисте приказали коментаре. Требаће мало дуже да се учитају.", + "View YouTube comments": "Прикажи коментаре са YouTube-а", + "View more comments on Reddit": "Прикажи још коментара на Reddit-у", "View `x` comments.": "", - "View Reddit comments": "", - "Hide replies": "", - "Show replies": "", - "Incorrect password": "", + "View Reddit comments": "Прикажи коментаре са Reddit-а", + "Hide replies": "Сакриј одговоре", + "Show replies": "Прикажи одговоре", + "Incorrect password": "Неисправна лозинка", "Quota exceeded, try again in a few hours": "", "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "", "Invalid TFA code": "", From 76173821147851b89bd726d16f9fbadeee0206cf Mon Sep 17 00:00:00 2001 From: Oymate Date: Tue, 23 Mar 2021 04:37:20 +0000 Subject: [PATCH 030/420] Update Bengali (Bangladesh) translation --- locales/bn_BD.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/locales/bn_BD.json b/locales/bn_BD.json index 8356424f..0662a87c 100644 --- a/locales/bn_BD.json +++ b/locales/bn_BD.json @@ -59,11 +59,11 @@ "Autoplay: ": "স্বয়ংক্রিয় চালু: ", "Play next by default: ": "ডিফল্টভাবে পরবর্তী চালাও: ", "Autoplay next video: ": "পরবর্তী ভিডিও স্বয়ংক্রিয়ভাবে চালাও: ", - "Listen by default: ": "", - "Proxy videos: ": "", - "Default speed: ": "", - "Preferred video quality: ": "", - "Player volume: ": "", + "Listen by default: ": "সহজাতভাবে শোনো: ", + "Proxy videos: ": "ভিডিও প্রক্সি করো: ", + "Default speed: ": "সহজাত গতি: ", + "Preferred video quality: ": "পছন্দের ভিডিও মান: ", + "Player volume: ": "প্লেয়ার শব্দের মাত্রা: ", "Default comments: ": "", "youtube": "", "reddit": "", From b4bfe2778699d0ee9973b9a2ff598d7d7523abc6 Mon Sep 17 00:00:00 2001 From: HackerNCoder Date: Thu, 25 Mar 2021 17:59:04 +0000 Subject: [PATCH 031/420] Update Danish translation --- locales/da.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/da.json b/locales/da.json index 1944e47b..ac60862c 100644 --- a/locales/da.json +++ b/locales/da.json @@ -44,7 +44,7 @@ "Export data as JSON": "Exporter data som JSON", "Delete account?": "Slet konto?", "History": "Historik", - "An alternative front-end to YouTube": "", + "An alternative front-end to YouTube": "En alternativ forside til YouTube", "JavaScript license information": "JavaScript licens information", "source": "kilde", "Log in": "Log på", @@ -73,7 +73,7 @@ "Default comments: ": "Standard kommentarer: ", "youtube": "youtube", "reddit": "reddit", - "Default captions: ": "", + "Default captions: ": "Standard undertekster: ", "Fallback captions: ": "", "Show related videos: ": "", "Show annotations by default: ": "", From 75ec0b4fcf518da39738e7cb17dba446f3e634ce Mon Sep 17 00:00:00 2001 From: Hierax Swiftwing Date: Sun, 28 Mar 2021 16:26:59 +0200 Subject: [PATCH 032/420] Add Serbian translation --- locales/sr.json | 414 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 414 insertions(+) create mode 100644 locales/sr.json diff --git a/locales/sr.json b/locales/sr.json new file mode 100644 index 00000000..7ce450aa --- /dev/null +++ b/locales/sr.json @@ -0,0 +1,414 @@ +{ + "`x` subscribers": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "`x` videos": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "`x` playlists": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "LIVE": "", + "Shared `x` ago": "", + "Unsubscribe": "", + "Subscribe": "", + "View channel on YouTube": "", + "View playlist on YouTube": "", + "newest": "", + "oldest": "", + "popular": "", + "last": "", + "Next page": "", + "Previous page": "", + "Clear watch history?": "", + "New password": "", + "New passwords must match": "", + "Cannot change password for Google accounts": "", + "Authorize token?": "", + "Authorize token for `x`?": "", + "Yes": "", + "No": "", + "Import and Export Data": "", + "Import": "", + "Import Invidious data": "", + "Import YouTube subscriptions": "", + "Import FreeTube subscriptions (.db)": "", + "Import NewPipe subscriptions (.json)": "", + "Import NewPipe data (.zip)": "", + "Export": "", + "Export subscriptions as OPML": "", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "", + "Export data as JSON": "", + "Delete account?": "", + "History": "", + "An alternative front-end to YouTube": "", + "JavaScript license information": "", + "source": "", + "Log in": "", + "Log in/register": "", + "Log in with Google": "", + "User ID": "", + "Password": "", + "Time (h:mm:ss):": "", + "Text CAPTCHA": "", + "Image CAPTCHA": "", + "Sign In": "", + "Register": "", + "E-mail": "", + "Google verification code": "", + "Preferences": "", + "Player preferences": "", + "Always loop: ": "", + "Autoplay: ": "", + "Play next by default: ": "", + "Autoplay next video: ": "", + "Listen by default: ": "", + "Proxy videos: ": "", + "Default speed: ": "", + "Preferred video quality: ": "", + "Player volume: ": "", + "Default comments: ": "", + "youtube": "", + "reddit": "", + "Default captions: ": "", + "Fallback captions: ": "", + "Show related videos: ": "", + "Show annotations by default: ": "", + "Visual preferences": "", + "Player style: ": "", + "Dark mode: ": "", + "Theme: ": "", + "dark": "", + "light": "", + "Thin mode: ": "", + "Subscription preferences": "", + "Show annotations by default for subscribed channels: ": "", + "Redirect homepage to feed: ": "", + "Number of videos shown in feed: ": "", + "Sort videos by: ": "", + "published": "", + "published - reverse": "", + "alphabetically": "", + "alphabetically - reverse": "", + "channel name": "", + "channel name - reverse": "", + "Only show latest video from channel: ": "", + "Only show latest unwatched video from channel: ": "", + "Only show unwatched: ": "", + "Only show notifications (if there are any): ": "", + "Enable web notifications": "", + "`x` uploaded a video": "", + "`x` is live": "", + "Data preferences": "", + "Clear watch history": "", + "Import/export data": "", + "Change password": "", + "Manage subscriptions": "", + "Manage tokens": "", + "Watch history": "", + "Delete account": "", + "Administrator preferences": "", + "Default homepage: ": "", + "Feed menu: ": "", + "Top enabled: ": "", + "CAPTCHA enabled: ": "", + "Login enabled: ": "", + "Registration enabled: ": "", + "Report statistics: ": "", + "Save preferences": "", + "Subscription manager": "", + "Token manager": "", + "Token": "", + "`x` subscriptions": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "`x` tokens": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "Import/export": "", + "unsubscribe": "", + "revoke": "", + "Subscriptions": "", + "`x` unseen notifications": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "search": "", + "Log out": "", + "Released under the AGPLv3 by Omar Roth.": "", + "Source available here.": "", + "View JavaScript license information.": "", + "View privacy policy.": "", + "Trending": "", + "Public": "", + "Unlisted": "", + "Private": "", + "View all playlists": "", + "Updated `x` ago": "", + "Delete playlist `x`?": "", + "Delete playlist": "", + "Create playlist": "", + "Title": "", + "Playlist privacy": "", + "Editing playlist `x`": "", + "Watch on YouTube": "", + "Hide annotations": "", + "Show annotations": "", + "Genre: ": "", + "License: ": "", + "Family friendly? ": "", + "Wilson score: ": "", + "Engagement: ": "", + "Whitelisted regions: ": "", + "Blacklisted regions: ": "", + "Shared `x`": "", + "`x` views": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "Premieres in `x`": "", + "Premieres `x`": "", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "", + "View YouTube comments": "", + "View more comments on Reddit": "", + "View `x` comments": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "View Reddit comments": "", + "Hide replies": "", + "Show replies": "", + "Incorrect password": "", + "Quota exceeded, try again in a few hours": "", + "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "", + "Invalid TFA code": "", + "Login failed. This may be because two-factor authentication is not turned on for your account.": "", + "Wrong answer": "", + "Erroneous CAPTCHA": "", + "CAPTCHA is a required field": "", + "User ID is a required field": "", + "Password is a required field": "", + "Wrong username or password": "", + "Please sign in using 'Log in with Google'": "", + "Password cannot be empty": "", + "Password cannot be longer than 55 characters": "", + "Please log in": "", + "Invidious Private Feed for `x`": "", + "channel:`x`": "", + "Deleted or invalid channel": "", + "This channel does not exist.": "", + "Could not get channel info.": "", + "Could not fetch comments": "", + "View `x` replies": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "`x` ago": "", + "Load more": "", + "`x` points": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "Could not create mix.": "", + "Empty playlist": "", + "Not a playlist.": "", + "Playlist does not exist.": "", + "Could not pull trending pages.": "", + "Hidden field \"challenge\" is a required field": "", + "Hidden field \"token\" is a required field": "", + "Erroneous challenge": "", + "Erroneous token": "", + "No such user": "", + "Token is expired, please try again": "", + "English": "", + "English (auto-generated)": "", + "Afrikaans": "", + "Albanian": "", + "Amharic": "", + "Arabic": "", + "Armenian": "", + "Azerbaijani": "", + "Bangla": "", + "Basque": "", + "Belarusian": "", + "Bosnian": "", + "Bulgarian": "", + "Burmese": "", + "Catalan": "", + "Cebuano": "", + "Chinese (Simplified)": "", + "Chinese (Traditional)": "", + "Corsican": "", + "Croatian": "", + "Czech": "", + "Danish": "", + "Dutch": "", + "Esperanto": "", + "Estonian": "", + "Filipino": "", + "Finnish": "", + "French": "", + "Galician": "", + "Georgian": "", + "German": "", + "Greek": "", + "Gujarati": "", + "Haitian Creole": "", + "Hausa": "", + "Hawaiian": "", + "Hebrew": "", + "Hindi": "", + "Hmong": "", + "Hungarian": "", + "Icelandic": "", + "Igbo": "", + "Indonesian": "", + "Irish": "", + "Italian": "", + "Japanese": "", + "Javanese": "", + "Kannada": "", + "Kazakh": "", + "Khmer": "", + "Korean": "", + "Kurdish": "", + "Kyrgyz": "", + "Lao": "", + "Latin": "", + "Latvian": "", + "Lithuanian": "", + "Luxembourgish": "", + "Macedonian": "", + "Malagasy": "", + "Malay": "", + "Malayalam": "", + "Maltese": "", + "Maori": "", + "Marathi": "", + "Mongolian": "", + "Nepali": "", + "Norwegian Bokmål": "", + "Nyanja": "", + "Pashto": "", + "Persian": "", + "Polish": "", + "Portuguese": "", + "Punjabi": "", + "Romanian": "", + "Russian": "", + "Samoan": "", + "Scottish Gaelic": "", + "Serbian": "", + "Shona": "", + "Sindhi": "", + "Sinhala": "", + "Slovak": "", + "Slovenian": "", + "Somali": "", + "Southern Sotho": "", + "Spanish": "", + "Spanish (Latin America)": "", + "Sundanese": "", + "Swahili": "", + "Swedish": "", + "Tajik": "", + "Tamil": "", + "Telugu": "", + "Thai": "", + "Turkish": "", + "Ukrainian": "", + "Urdu": "", + "Uzbek": "", + "Vietnamese": "", + "Welsh": "", + "Western Frisian": "", + "Xhosa": "", + "Yiddish": "", + "Yoruba": "", + "Zulu": "", + "`x` years": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "`x` months": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "`x` weeks": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "`x` days": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "`x` hours": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "`x` minutes": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "`x` seconds": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "Fallback comments: ": "", + "Popular": "", + "Top": "", + "About": "", + "Rating: ": "", + "Language: ": "", + "View as playlist": "", + "Default": "", + "Music": "", + "Gaming": "", + "News": "", + "Movies": "", + "Download": "", + "Download as: ": "", + "%A %B %-d, %Y": "", + "(edited)": "", + "YouTube comment permalink": "", + "permalink": "", + "`x` marked it with a ❤": "", + "Audio mode": "", + "Video mode": "", + "Videos": "", + "Playlists": "", + "Community": "", + "relevance": "", + "rating": "", + "date": "", + "views": "", + "content_type": "", + "duration": "", + "features": "", + "sort": "", + "hour": "", + "today": "", + "week": "", + "month": "", + "year": "", + "video": "", + "channel": "", + "playlist": "", + "movie": "", + "show": "", + "hd": "", + "subtitles": "", + "creative_commons": "", + "3d": "", + "live": "", + "4k": "", + "location": "", + "hdr": "", + "filter": "", + "Current version: ": "" +} From a2f5435c482d543d7bceec29aafd2f7df06e3b19 Mon Sep 17 00:00:00 2001 From: Hierax Swiftwing Date: Sun, 28 Mar 2021 15:49:49 +0000 Subject: [PATCH 033/420] Update Serbian translation --- locales/sr.json | 124 ++++++++++++++++++++++++------------------------ 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/locales/sr.json b/locales/sr.json index 7ce450aa..dd8fb2fc 100644 --- a/locales/sr.json +++ b/locales/sr.json @@ -1,72 +1,72 @@ { "`x` subscribers": { - "([^.,0-9]|^)1([^.,0-9]|$)": "", - "": "" + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` пратилаца.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` пратилаца." }, "`x` videos": { - "([^.,0-9]|^)1([^.,0-9]|$)": "", - "": "" + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` видео записа.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` видео записа." }, "`x` playlists": { - "([^.,0-9]|^)1([^.,0-9]|$)": "", - "": "" + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` списака извођења.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` списака извођења." }, - "LIVE": "", - "Shared `x` ago": "", - "Unsubscribe": "", - "Subscribe": "", - "View channel on YouTube": "", - "View playlist on YouTube": "", - "newest": "", - "oldest": "", - "popular": "", - "last": "", - "Next page": "", - "Previous page": "", - "Clear watch history?": "", - "New password": "", - "New passwords must match": "", - "Cannot change password for Google accounts": "", - "Authorize token?": "", - "Authorize token for `x`?": "", - "Yes": "", - "No": "", - "Import and Export Data": "", - "Import": "", - "Import Invidious data": "", - "Import YouTube subscriptions": "", - "Import FreeTube subscriptions (.db)": "", - "Import NewPipe subscriptions (.json)": "", - "Import NewPipe data (.zip)": "", - "Export": "", - "Export subscriptions as OPML": "", - "Export subscriptions as OPML (for NewPipe & FreeTube)": "", - "Export data as JSON": "", - "Delete account?": "", - "History": "", - "An alternative front-end to YouTube": "", - "JavaScript license information": "", - "source": "", - "Log in": "", - "Log in/register": "", - "Log in with Google": "", - "User ID": "", - "Password": "", - "Time (h:mm:ss):": "", - "Text CAPTCHA": "", - "Image CAPTCHA": "", - "Sign In": "", - "Register": "", - "E-mail": "", - "Google verification code": "", - "Preferences": "", - "Player preferences": "", - "Always loop: ": "", - "Autoplay: ": "", - "Play next by default: ": "", - "Autoplay next video: ": "", - "Listen by default: ": "", - "Proxy videos: ": "", + "LIVE": "УЖИВО", + "Shared `x` ago": "Подељено пре `x`", + "Unsubscribe": "Прекини праћење", + "Subscribe": "Прати", + "View channel on YouTube": "Погледај канал на YouTube-у", + "View playlist on YouTube": "Погледај списак извођења на YouTube-у", + "newest": "најновије", + "oldest": "најстарије", + "popular": "гласовито", + "last": "последње", + "Next page": "Следећа страница", + "Previous page": "Претходна страница", + "Clear watch history?": "Избрисати повест прегледања?", + "New password": "Нова запорка", + "New passwords must match": "Нове запорке морају бити истоветне", + "Cannot change password for Google accounts": "Није могуће променити запорку за Google налоге", + "Authorize token?": "Овласти токен?", + "Authorize token for `x`?": "Овласти токен за `x`?", + "Yes": "Да", + "No": "Не", + "Import and Export Data": "Увоз и извоз података", + "Import": "Увези", + "Import Invidious data": "Увези податке са Invidious-а", + "Import YouTube subscriptions": "Увези праћења са YouTube-а", + "Import FreeTube subscriptions (.db)": "Увези праћења са FreeTube-а (.db)", + "Import NewPipe subscriptions (.json)": "Увези праћења са NewPipe-а (.json)", + "Import NewPipe data (.zip)": "Увези податке са NewPipe-а (.zip)", + "Export": "Извези", + "Export subscriptions as OPML": "Извези праћења као OPML датотеку", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "Извези праћења као OPML датотеку (за NewPipe и FreeTube)", + "Export data as JSON": "Извези податке као JSON датотеку", + "Delete account?": "Избрисати рачун?", + "History": "Повест", + "An alternative front-end to YouTube": "Заменски кориснички слој за YouTube", + "JavaScript license information": "Извештај о JavaScript одобрењу", + "source": "извор", + "Log in": "Пријави се", + "Log in/register": "Пријави се/Отвори налог", + "Log in with Google": "Пријави се помоћу Google-а", + "User ID": "Кориснички ИД", + "Password": "Запорка", + "Time (h:mm:ss):": "Време (ч:мм:сс):", + "Text CAPTCHA": "Знаковни CAPTCHA", + "Image CAPTCHA": "Сликовни CAPTCHA", + "Sign In": "Пријава", + "Register": "Отвори налог", + "E-mail": "Е-пошта", + "Google verification code": "Google-ов оверни кôд", + "Preferences": "Подешавања", + "Player preferences": "Подешавања репродуктора", + "Always loop: ": "Увек понављај: ", + "Autoplay: ": "Самопуштање: ", + "Play next by default: ": "Увек подразумевано пуштај следеће: ", + "Autoplay next video: ": "Самопуштање следећег видео записа: ", + "Listen by default: ": "Увек подразумевано укључен само звук: ", + "Proxy videos: ": "Приказ видео записа преко посредника: ", "Default speed: ": "", "Preferred video quality: ": "", "Player volume: ": "", From 608313c1d1e0f5ec9b4ce9b1d6303f03530656d8 Mon Sep 17 00:00:00 2001 From: syeopite Date: Mon, 29 Mar 2021 17:37:12 -0700 Subject: [PATCH 034/420] Update regex expressions to handle unexpected '};' --- src/invidious/helpers/helpers.cr | 2 +- src/invidious/videos.cr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index 5d127e1a..5e49afb7 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -678,7 +678,7 @@ def create_notification_stream(env, topics, connection_channel) end def extract_initial_data(body) : Hash(String, JSON::Any) - return JSON.parse(body.match(/(window\["ytInitialData"\]|var\s*ytInitialData)\s*=\s*(?\{.*?\});/mx).try &.["info"] || "{}").as_h + return JSON.parse(body.match(/(window\["ytInitialData"\]|var\s*ytInitialData)\s*=\s*(?{.*?});<\/script>/mx).try &.["info"] || "{}").as_h end def proxy_file(response, env) diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index e6d4c764..2b793a1b 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -818,7 +818,7 @@ end def extract_polymer_config(body) params = {} of String => JSON::Any - player_response = body.match(/(window\["ytInitialPlayerResponse"\]|var\sytInitialPlayerResponse)\s*=\s*(?{.*?});/m) + player_response = body.match(/(window\["ytInitialPlayerResponse"\]|var\sytInitialPlayerResponse)\s*=\s*(?{.*?});\s*var\s*meta/m) .try { |r| JSON.parse(r["info"]).as_h } if body.includes?("To continue with your YouTube experience, please fill out the form below.") || From 739f6105073245b051aa08354e9a4f15c25a3572 Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 31 Mar 2021 14:57:00 -0700 Subject: [PATCH 035/420] Add new YT consent cookie to every request --- src/invidious/helpers/utils.cr | 1 + 1 file changed, 1 insertion(+) diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 2c95a373..31bbda87 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -9,6 +9,7 @@ def add_yt_headers(request) return if request.resource.starts_with? "/sorry/index" request.headers["x-youtube-client-name"] ||= "1" request.headers["x-youtube-client-version"] ||= "2.20200609" + request.headers["cookie"] = "CONSENT=YES+cb.20210328-17-p0.en; " # New YT consent cookie for EU servers if !CONFIG.cookies.empty? request.headers["cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}" end From e08bea5f51c4cfb8aff790a2374e01e3f6ef48bf Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 31 Mar 2021 15:34:29 -0700 Subject: [PATCH 036/420] Fix lint --- src/invidious/helpers/utils.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 31bbda87..7a03b962 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -9,7 +9,7 @@ def add_yt_headers(request) return if request.resource.starts_with? "/sorry/index" request.headers["x-youtube-client-name"] ||= "1" request.headers["x-youtube-client-version"] ||= "2.20200609" - request.headers["cookie"] = "CONSENT=YES+cb.20210328-17-p0.en; " # New YT consent cookie for EU servers + request.headers["cookie"] = "CONSENT=YES+cb.20210328-17-p0.en; " # New YT consent cookie for EU servers if !CONFIG.cookies.empty? request.headers["cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}" end From 87c25f83a491a61727049876cd8343bdb3929103 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 1 Apr 2021 00:23:59 +0000 Subject: [PATCH 037/420] Fix API giving ytimg instead of instance URLs for thumbnails --- src/invidious/videos.cr | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index e6d4c764..38646311 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -1150,15 +1150,15 @@ end def build_thumbnails(id) return { - {name: "maxres", host: "#{HOST_URL}", url: "maxres", height: 720, width: 1280}, - {name: "maxresdefault", host: "https://i.ytimg.com", url: "maxresdefault", height: 720, width: 1280}, - {name: "sddefault", host: "https://i.ytimg.com", url: "sddefault", height: 480, width: 640}, - {name: "high", host: "https://i.ytimg.com", url: "hqdefault", height: 360, width: 480}, - {name: "medium", host: "https://i.ytimg.com", url: "mqdefault", height: 180, width: 320}, - {name: "default", host: "https://i.ytimg.com", url: "default", height: 90, width: 120}, - {name: "start", host: "https://i.ytimg.com", url: "1", height: 90, width: 120}, - {name: "middle", host: "https://i.ytimg.com", url: "2", height: 90, width: 120}, - {name: "end", host: "https://i.ytimg.com", url: "3", height: 90, width: 120}, + {host: HOST_URL, height: 720, width: 1280, name: "maxres", url: "maxres"}, + {host: HOST_URL, height: 720, width: 1280, name: "maxresdefault", url: "maxresdefault"}, + {host: HOST_URL, height: 480, width: 640, name: "sddefault", url: "sddefault"}, + {host: HOST_URL, height: 360, width: 480, name: "high", url: "hqdefault"}, + {host: HOST_URL, height: 180, width: 320, name: "medium", url: "mqdefault"}, + {host: HOST_URL, height: 90, width: 120, name: "default", url: "default"}, + {host: HOST_URL, height: 90, width: 120, name: "start", url: "1"}, + {host: HOST_URL, height: 90, width: 120, name: "middle", url: "2"}, + {host: HOST_URL, height: 90, width: 120, name: "end", url: "3"}, } end From b794c5cfcffcc6b75dec3d61197db269128e5e48 Mon Sep 17 00:00:00 2001 From: TheFrenchGhosty <47571719+TheFrenchGhosty@users.noreply.github.com> Date: Thu, 1 Apr 2021 15:59:24 +0000 Subject: [PATCH 038/420] Set the request cookie to "YES+" --- src/invidious/helpers/utils.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 7a03b962..92d8a7bb 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -9,7 +9,7 @@ def add_yt_headers(request) return if request.resource.starts_with? "/sorry/index" request.headers["x-youtube-client-name"] ||= "1" request.headers["x-youtube-client-version"] ||= "2.20200609" - request.headers["cookie"] = "CONSENT=YES+cb.20210328-17-p0.en; " # New YT consent cookie for EU servers + request.headers["cookie"] = "CONSENT=YES+" # New YT consent cookie for EU servers if !CONFIG.cookies.empty? request.headers["cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}" end From 62e46b7a366fa10d376a5a95eb9051f581fa7f60 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 1 Apr 2021 18:46:49 +0000 Subject: [PATCH 039/420] Fix missing last page in playlists --- 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 73c14155..1f7fa27d 100644 --- a/src/invidious/routes/playlists.cr +++ b/src/invidious/routes/playlists.cr @@ -434,7 +434,7 @@ class Invidious::Routes::Playlists < Invidious::Routes::BaseRoute end page_count = (playlist.video_count / 100).to_i - page_count = 1 if page_count == 0 + page_count += 1 if (playlist.video_count % 100) > 0 if page > page_count return env.redirect "/playlist?list=#{plid}&page=#{page_count}" From 20b961c1c8b59a608ad7b28754d1976b6b411932 Mon Sep 17 00:00:00 2001 From: syeopite Date: Fri, 2 Apr 2021 17:08:55 -0700 Subject: [PATCH 040/420] Preserve original cookies --- src/invidious/helpers/utils.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 92d8a7bb..67f496df 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -9,7 +9,8 @@ def add_yt_headers(request) return if request.resource.starts_with? "/sorry/index" request.headers["x-youtube-client-name"] ||= "1" request.headers["x-youtube-client-version"] ||= "2.20200609" - request.headers["cookie"] = "CONSENT=YES+" # New YT consent cookie for EU servers + # Preserve original cookies and add new YT consent cookie for EU servers + request.headers["cookie"] = "#{request.headers["cookie"]?}; CONSENT=YES+" if !CONFIG.cookies.empty? request.headers["cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}" end From fe4eef585523ea1e805afdfa106b7dde1f458f7e Mon Sep 17 00:00:00 2001 From: syeopite Date: Fri, 2 Apr 2021 20:58:39 -0700 Subject: [PATCH 041/420] Fix channel info extract for 'video game' channels --- src/invidious/channels.cr | 76 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index 47dfcbd6..014df8d5 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -821,6 +821,19 @@ def get_about_info(ucid, locale) raise ChannelRedirect.new(channel_id: browse_endpoint["browseId"].to_s) end + auto_generated = false + if !initdata.has_key?("metadata") + auto_generated = true + end + + if auto_generated + return get_auto_generated_channel_info(initdata, about, ucid) + else + return get_normal_channel_info(initdata, about) + end +end + +def get_normal_channel_info(initdata, about) author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s author_thumbnail = initdata["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].as_s @@ -929,6 +942,69 @@ def get_about_info(ucid, locale) }) end +def get_auto_generated_channel_info(initdata, about, ucid) + author = initdata["header"]["interactiveTabbedHeaderRenderer"]["title"]["simpleText"].as_s + author_url = initdata["microformat"]["microformatDataRenderer"]["urlCanonical"].as_s + author_thumbnail = initdata["header"]["interactiveTabbedHeaderRenderer"]["boxArt"]["thumbnails"][0]["url"].as_s + + # Raises a KeyError on failure. + banners = initdata["header"]["interactiveTabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]? + banner = banners.try &.[-1]?.try &.["url"].as_s? + + description = initdata["header"]["interactiveTabbedHeaderRenderer"]["description"]["simpleText"].as_s + description_html = HTML.escape(description).gsub("\n", "
") + + paid = false + is_family_friendly = initdata["microformat"]["microformatDataRenderer"]["familySafe"].as_bool + allowed_regions = initdata["microformat"]["microformatDataRenderer"]["availableCountries"].as_a.map { |a| a.as_s } + + related_channels = [] of AboutRelatedChannel + + total_views = 0_i64 + joined = Time.unix(0) + tabs = [] of String + auto_generated = true + + tabs_json = initdata["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]?.try &.as_a? + if !tabs_json.nil? + # Retrieve information from the tabs array. The index we are looking for varies between channels. + tabs_json.each do |node| + # Try to find the about section which is located in only one of the tabs. + channel_about_meta = node["tabRenderer"]?.try &.["content"]?.try &.["sectionListRenderer"]? + .try &.["contents"]?.try &.[0]?.try &.["itemSectionRenderer"]?.try &.["contents"]? + .try &.[0]?.try &.["channelAboutFullMetadataRenderer"]? + + if !channel_about_meta.nil? + # The joined text is split to several sub strings. The reduce joins those strings before parsing the date. + joined = channel_about_meta["joinedDateText"]?.try &.["runs"]?.try &.as_a.reduce("") { |acc, node| acc + node["text"].as_s } + .try { |text| Time.parse(text, "Joined %b %-d, %Y", Time::Location.local) } || Time.unix(0) + end + end + tabs = tabs_json.reject { |node| node["tabRenderer"]?.nil? }.map { |node| node["tabRenderer"]["title"].as_s.downcase } + end + + sub_count = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["subscriberCountText"]?.try &.["simpleText"]?.try &.as_s? + .try { |text| short_text_to_number(text.split(" ")[0]) } || 0 + + AboutChannel.new({ + ucid: ucid, + author: author, + auto_generated: auto_generated, + author_url: author_url, + author_thumbnail: author_thumbnail, + banner: banner, + description_html: description_html, + paid: paid, + total_views: total_views, + sub_count: sub_count, + joined: joined, + is_family_friendly: is_family_friendly, + allowed_regions: allowed_regions, + related_channels: related_channels, + tabs: tabs, + }) +end + 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 b4a6cbbd09f122823feae418e898c8d13434ffd8 Mon Sep 17 00:00:00 2001 From: syeopite Date: Sat, 3 Apr 2021 20:54:10 -0700 Subject: [PATCH 042/420] Merge info extract functions back to one --- src/invidious/channels.cr | 170 +++++++++++++------------------------- 1 file changed, 59 insertions(+), 111 deletions(-) diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index 014df8d5..3109b508 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -822,75 +822,86 @@ def get_about_info(ucid, locale) end auto_generated = false + # Check for special auto generated gaming channels if !initdata.has_key?("metadata") auto_generated = true end if auto_generated - return get_auto_generated_channel_info(initdata, about, ucid) + author = initdata["header"]["interactiveTabbedHeaderRenderer"]["title"]["simpleText"].as_s + author_url = initdata["microformat"]["microformatDataRenderer"]["urlCanonical"].as_s + author_thumbnail = initdata["header"]["interactiveTabbedHeaderRenderer"]["boxArt"]["thumbnails"][0]["url"].as_s + + # Raises a KeyError on failure. + banners = initdata["header"]["interactiveTabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]? + banner = banners.try &.[-1]?.try &.["url"].as_s? + + description = initdata["header"]["interactiveTabbedHeaderRenderer"]["description"]["simpleText"].as_s + description_html = HTML.escape(description).gsub("\n", "
") + + paid = false + is_family_friendly = initdata["microformat"]["microformatDataRenderer"]["familySafe"].as_bool + allowed_regions = initdata["microformat"]["microformatDataRenderer"]["availableCountries"].as_a.map { |a| a.as_s } + + related_channels = [] of AboutRelatedChannel else - return get_normal_channel_info(initdata, about) - end -end + author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s + author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s + author_thumbnail = initdata["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].as_s -def get_normal_channel_info(initdata, about) - author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s - author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s - author_thumbnail = initdata["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].as_s + ucid = initdata["metadata"]["channelMetadataRenderer"]["externalId"].as_s - ucid = initdata["metadata"]["channelMetadataRenderer"]["externalId"].as_s + # Raises a KeyError on failure. + banners = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]? + banner = banners.try &.[-1]?.try &.["url"].as_s? - # Raises a KeyError on failure. - banners = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]? - banner = banners.try &.[-1]?.try &.["url"].as_s? + # if banner.includes? "channels/c4/default_banner" + # banner = nil + # end - # if banner.includes? "channels/c4/default_banner" - # banner = nil - # end + description = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]?.try &.as_s? || "" + description_html = HTML.escape(description).gsub("\n", "
") - description = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]?.try &.as_s? || "" - description_html = HTML.escape(description).gsub("\n", "
") + paid = about.xpath_node(%q(//meta[@itemprop="paid"])).not_nil!["content"] == "True" + is_family_friendly = about.xpath_node(%q(//meta[@itemprop="isFamilyFriendly"])).not_nil!["content"] == "True" + allowed_regions = about.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).not_nil!["content"].split(",") - paid = about.xpath_node(%q(//meta[@itemprop="paid"])).not_nil!["content"] == "True" - is_family_friendly = about.xpath_node(%q(//meta[@itemprop="isFamilyFriendly"])).not_nil!["content"] == "True" - allowed_regions = about.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).not_nil!["content"].split(",") + related_channels = initdata["contents"]["twoColumnBrowseResultsRenderer"] + .["secondaryContents"]?.try &.["browseSecondaryContentsRenderer"]["contents"][0]? + .try &.["verticalChannelSectionRenderer"]?.try &.["items"]?.try &.as_a.map do |node| + renderer = node["miniChannelRenderer"]? + related_id = renderer.try &.["channelId"]?.try &.as_s? + related_id ||= "" - related_channels = initdata["contents"]["twoColumnBrowseResultsRenderer"] - .["secondaryContents"]?.try &.["browseSecondaryContentsRenderer"]["contents"][0]? - .try &.["verticalChannelSectionRenderer"]?.try &.["items"]?.try &.as_a.map do |node| - renderer = node["miniChannelRenderer"]? - related_id = renderer.try &.["channelId"]?.try &.as_s? - related_id ||= "" + related_title = renderer.try &.["title"]?.try &.["simpleText"]?.try &.as_s? + related_title ||= "" - related_title = renderer.try &.["title"]?.try &.["simpleText"]?.try &.as_s? - related_title ||= "" + related_author_url = renderer.try &.["navigationEndpoint"]?.try &.["commandMetadata"]?.try &.["webCommandMetadata"]? + .try &.["url"]?.try &.as_s? + related_author_url ||= "" - related_author_url = renderer.try &.["navigationEndpoint"]?.try &.["commandMetadata"]?.try &.["webCommandMetadata"]? - .try &.["url"]?.try &.as_s? - related_author_url ||= "" + related_author_thumbnails = renderer.try &.["thumbnail"]?.try &.["thumbnails"]?.try &.as_a? + related_author_thumbnails ||= [] of JSON::Any - related_author_thumbnails = renderer.try &.["thumbnail"]?.try &.["thumbnails"]?.try &.as_a? - related_author_thumbnails ||= [] of JSON::Any + related_author_thumbnail = "" + if related_author_thumbnails.size > 0 + related_author_thumbnail = related_author_thumbnails[-1]["url"]?.try &.as_s? + related_author_thumbnail ||= "" + end - related_author_thumbnail = "" - if related_author_thumbnails.size > 0 - related_author_thumbnail = related_author_thumbnails[-1]["url"]?.try &.as_s? - related_author_thumbnail ||= "" + AboutRelatedChannel.new({ + ucid: related_id, + author: related_title, + author_url: related_author_url, + author_thumbnail: related_author_thumbnail, + }) end - - AboutRelatedChannel.new({ - ucid: related_id, - author: related_title, - author_url: related_author_url, - author_thumbnail: related_author_thumbnail, - }) - end - related_channels ||= [] of AboutRelatedChannel + related_channels ||= [] of AboutRelatedChannel + end total_views = 0_i64 joined = Time.unix(0) tabs = [] of String - auto_generated = false tabs_json = initdata["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]?.try &.as_a? if !tabs_json.nil? @@ -908,7 +919,7 @@ def get_normal_channel_info(initdata, about) joined = channel_about_meta["joinedDateText"]?.try &.["runs"]?.try &.as_a.reduce("") { |acc, node| acc + node["text"].as_s } .try { |text| Time.parse(text, "Joined %b %-d, %Y", Time::Location.local) } || Time.unix(0) - # Auto-generated channels + # Normal Auto-generated channels # https://support.google.com/youtube/answer/2579942 # For auto-generated channels, channel_about_meta only has ["description"]["simpleText"] and ["primaryLinks"][0]["title"]["simpleText"] if (channel_about_meta["primaryLinks"]?.try &.size || 0) == 1 && (channel_about_meta["primaryLinks"][0]?) && @@ -942,69 +953,6 @@ def get_normal_channel_info(initdata, about) }) end -def get_auto_generated_channel_info(initdata, about, ucid) - author = initdata["header"]["interactiveTabbedHeaderRenderer"]["title"]["simpleText"].as_s - author_url = initdata["microformat"]["microformatDataRenderer"]["urlCanonical"].as_s - author_thumbnail = initdata["header"]["interactiveTabbedHeaderRenderer"]["boxArt"]["thumbnails"][0]["url"].as_s - - # Raises a KeyError on failure. - banners = initdata["header"]["interactiveTabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]? - banner = banners.try &.[-1]?.try &.["url"].as_s? - - description = initdata["header"]["interactiveTabbedHeaderRenderer"]["description"]["simpleText"].as_s - description_html = HTML.escape(description).gsub("\n", "
") - - paid = false - is_family_friendly = initdata["microformat"]["microformatDataRenderer"]["familySafe"].as_bool - allowed_regions = initdata["microformat"]["microformatDataRenderer"]["availableCountries"].as_a.map { |a| a.as_s } - - related_channels = [] of AboutRelatedChannel - - total_views = 0_i64 - joined = Time.unix(0) - tabs = [] of String - auto_generated = true - - tabs_json = initdata["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]?.try &.as_a? - if !tabs_json.nil? - # Retrieve information from the tabs array. The index we are looking for varies between channels. - tabs_json.each do |node| - # Try to find the about section which is located in only one of the tabs. - channel_about_meta = node["tabRenderer"]?.try &.["content"]?.try &.["sectionListRenderer"]? - .try &.["contents"]?.try &.[0]?.try &.["itemSectionRenderer"]?.try &.["contents"]? - .try &.[0]?.try &.["channelAboutFullMetadataRenderer"]? - - if !channel_about_meta.nil? - # The joined text is split to several sub strings. The reduce joins those strings before parsing the date. - joined = channel_about_meta["joinedDateText"]?.try &.["runs"]?.try &.as_a.reduce("") { |acc, node| acc + node["text"].as_s } - .try { |text| Time.parse(text, "Joined %b %-d, %Y", Time::Location.local) } || Time.unix(0) - end - end - tabs = tabs_json.reject { |node| node["tabRenderer"]?.nil? }.map { |node| node["tabRenderer"]["title"].as_s.downcase } - end - - sub_count = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["subscriberCountText"]?.try &.["simpleText"]?.try &.as_s? - .try { |text| short_text_to_number(text.split(" ")[0]) } || 0 - - AboutChannel.new({ - ucid: ucid, - author: author, - auto_generated: auto_generated, - author_url: author_url, - author_thumbnail: author_thumbnail, - banner: banner, - description_html: description_html, - paid: paid, - total_views: total_views, - sub_count: sub_count, - joined: joined, - is_family_friendly: is_family_friendly, - allowed_regions: allowed_regions, - related_channels: related_channels, - tabs: tabs, - }) -end - 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 e864c7541c29a571d3f1d186bf4014515b59110b Mon Sep 17 00:00:00 2001 From: syeopite Date: Sat, 3 Apr 2021 21:32:30 -0700 Subject: [PATCH 043/420] Hide header search bar when default_home is empty --- src/invidious/views/template.ecr | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 61b900e3..f7f00d2a 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -28,16 +28,19 @@