From 855202e40e09af1cb5fb372d4a2d05a61b3a9bb2 Mon Sep 17 00:00:00 2001 From: Gavin Johnson Date: Mon, 16 Jan 2023 15:40:38 -0800 Subject: [PATCH 01/41] added youtube playlist import; initial commit Signed-off-by: Gavin Johnson --- locales/en-US.json | 1 + src/invidious/user/imports.cr | 85 +++++++++++++++++++++++ src/invidious/views/user/data_control.ecr | 5 ++ 3 files changed, 91 insertions(+) diff --git a/locales/en-US.json b/locales/en-US.json index 12955665..c30a90db 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -33,6 +33,7 @@ "Import": "Import", "Import Invidious data": "Import Invidious JSON data", "Import YouTube subscriptions": "Import YouTube/OPML subscriptions", + "Import YouTube playlist": "Import YouTube playlist (.csv)", "Import FreeTube subscriptions (.db)": "Import FreeTube subscriptions (.db)", "Import NewPipe subscriptions (.json)": "Import NewPipe subscriptions (.json)", "Import NewPipe data (.zip)": "Import NewPipe data (.zip)", diff --git a/src/invidious/user/imports.cr b/src/invidious/user/imports.cr index 20ae0d47..870d083e 100644 --- a/src/invidious/user/imports.cr +++ b/src/invidious/user/imports.cr @@ -30,6 +30,75 @@ struct Invidious::User return subscriptions end + # Parse a youtube CSV playlist file and create the playlist + #NEW - Done + def parse_playlist_export_csv(user : User, csv_content : String) + rows = CSV.new(csv_content, headers: false) + if rows.size >= 2 + title = rows[1][4]?.try &.as_s?.try + descripton = rows[1][5]?.try &.as_s?.try + visibility = rows[1][6]?.try &.as_s?.try + + if visibility.compare("Public", case_insensitive: true) == 0 + privacy = PlaylistPrivacy:Public + else + privacy = PlaylistPrivacy:Private + end + + if title && privacy && user + playlist = create_playlist(title, privacy, user) + end + + if playlist && descripton + Invidious::Database::Playlists.update_description(playlist.id, description) + end + end + + return playlist + end + + # Parse a youtube CSV playlist file and add videos from it to a playlist + #NEW - done + def parse_playlist_videos_export_csv(playlist : Playlist, csv_content : String) + rows = CSV.new(csv_content, headers: false) + if rows.size >= 5 + offset = env.params.query["index"]?.try &.to_i? || 0 + row_counter = 0 + rows.each do |row| + if row_counter >= 4 + video_id = row[0]?.try &.as_s?.try + end + row_counter += 1 + next if !video_id + + begin + video = get_video(video_id) + rescue ex + next + end + + playlist_video = PlaylistVideo.new({ + title: video.title, + id: video.id, + author: video.author, + ucid: video.ucid, + length_seconds: video.length_seconds, + published: video.published, + plid: playlist.id, + live_now: video.live_now, + index: Random::Secure.rand(0_i64..Int64::MAX), + }) + + Invidious::Database::PlaylistVideos.insert(playlist_video) + Invidious::Database::Playlists.update_video_added(playlist.id, playlist_video.index) + end + + videos = get_playlist_videos(playlist, offset: offset) + end + + return videos + end + # ------------------- # Invidious # ------------------- @@ -149,6 +218,22 @@ struct Invidious::User return true end + # Import playlist from Youtube + # Returns success status + #NEW + def from_youtube_pl(user : User, body : String, filename : String, type : String) : Bool + extension = filename.split(".").last + + if extension == "csv" || type == "text/csv" + playlist = parse_playlist_export_csv(user, body) + playlist = parse_playlist_videos_export_csv(playlist, body) + else + return false + end + + return true + end + # ------------------- # Freetube # ------------------- diff --git a/src/invidious/views/user/data_control.ecr b/src/invidious/views/user/data_control.ecr index a451159f..0f8e8dae 100644 --- a/src/invidious/views/user/data_control.ecr +++ b/src/invidious/views/user/data_control.ecr @@ -21,6 +21,11 @@ +
+ + +
+
From 96344f28b4b842e915325aef64bc93fc9fc55387 Mon Sep 17 00:00:00 2001 From: Gavin Johnson Date: Sat, 28 Jan 2023 09:26:16 -0800 Subject: [PATCH 02/41] added youtube playlist import functionality. fixes issue #2114 Signed-off-by: Gavin Johnson --- locales/en-US.json | 2 +- src/invidious/routes/preferences.cr | 10 ++ src/invidious/user/imports.cr | 129 +++++++++++----------- src/invidious/views/feeds/playlists.ecr | 13 ++- src/invidious/views/user/data_control.ecr | 4 +- 5 files changed, 86 insertions(+), 72 deletions(-) diff --git a/locales/en-US.json b/locales/en-US.json index c30a90db..8f1ec58d 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -33,7 +33,7 @@ "Import": "Import", "Import Invidious data": "Import Invidious JSON data", "Import YouTube subscriptions": "Import YouTube/OPML subscriptions", - "Import YouTube playlist": "Import YouTube playlist (.csv)", + "Import YouTube playlist (.csv)": "Import YouTube playlist (.csv)", "Import FreeTube subscriptions (.db)": "Import FreeTube subscriptions (.db)", "Import NewPipe subscriptions (.json)": "Import NewPipe subscriptions (.json)", "Import NewPipe data (.zip)": "Import NewPipe data (.zip)", diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr index 570cba69..adac0068 100644 --- a/src/invidious/routes/preferences.cr +++ b/src/invidious/routes/preferences.cr @@ -310,6 +310,16 @@ module Invidious::Routes::PreferencesRoute response: error_template(415, "Invalid subscription file uploaded") ) end + # Gavin Johnson (thtmnisamnstr), 20230127: Call the Youtube playlist import function + when "import_youtube_pl" + filename = part.filename || "" + success = Invidious::User::Import.from_youtube_pl(user, body, filename, type) + + if !success + haltf(env, status_code: 415, + response: error_template(415, "Invalid playlist file uploaded") + ) + end when "import_freetube" Invidious::User::Import.from_freetube(user, body) when "import_newpipe_subscriptions" diff --git a/src/invidious/user/imports.cr b/src/invidious/user/imports.cr index 870d083e..fa1bbe7f 100644 --- a/src/invidious/user/imports.cr +++ b/src/invidious/user/imports.cr @@ -30,75 +30,72 @@ struct Invidious::User return subscriptions end - # Parse a youtube CSV playlist file and create the playlist - #NEW - Done + # Gavin Johnson (thtmnisamnstr), 20230127: Parse a youtube CSV playlist file and create the playlist def parse_playlist_export_csv(user : User, csv_content : String) - rows = CSV.new(csv_content, headers: false) - if rows.size >= 2 - title = rows[1][4]?.try &.as_s?.try - descripton = rows[1][5]?.try &.as_s?.try - visibility = rows[1][6]?.try &.as_s?.try - - if visibility.compare("Public", case_insensitive: true) == 0 - privacy = PlaylistPrivacy:Public - else - privacy = PlaylistPrivacy:Private - end + rows = CSV.new(csv_content, headers: true) + row_counter = 0 + playlist = uninitialized InvidiousPlaylist + title = uninitialized String + description = uninitialized String + visibility = uninitialized String + rows.each do |row| + if row_counter == 0 + title = row[4] + description = row[5] + visibility = row[6] - if title && privacy && user - playlist = create_playlist(title, privacy, user) - end + if visibility.compare("Public", case_insensitive: true) == 0 + privacy = PlaylistPrivacy::Public + else + privacy = PlaylistPrivacy::Private + end + + if title && privacy && user + playlist = create_playlist(title, privacy, user) + end + + if playlist && description + Invidious::Database::Playlists.update_description(playlist.id, description) + end - if playlist && descripton - Invidious::Database::Playlists.update_description(playlist.id, description) + row_counter += 1 + end + if row_counter > 0 && row_counter < 3 + row_counter += 1 + end + if row_counter >= 3 + if playlist + video_id = row[0] + row_counter += 1 + next if !video_id + + begin + video = get_video(video_id) + rescue ex + next + end + + playlist_video = PlaylistVideo.new({ + title: video.title, + id: video.id, + author: video.author, + ucid: video.ucid, + length_seconds: video.length_seconds, + published: video.published, + plid: playlist.id, + live_now: video.live_now, + index: Random::Secure.rand(0_i64..Int64::MAX), + }) + + Invidious::Database::PlaylistVideos.insert(playlist_video) + Invidious::Database::Playlists.update_video_added(playlist.id, playlist_video.index) + end end end return playlist end - # Parse a youtube CSV playlist file and add videos from it to a playlist - #NEW - done - def parse_playlist_videos_export_csv(playlist : Playlist, csv_content : String) - rows = CSV.new(csv_content, headers: false) - if rows.size >= 5 - offset = env.params.query["index"]?.try &.to_i? || 0 - row_counter = 0 - rows.each do |row| - if row_counter >= 4 - video_id = row[0]?.try &.as_s?.try - end - row_counter += 1 - next if !video_id - - begin - video = get_video(video_id) - rescue ex - next - end - - playlist_video = PlaylistVideo.new({ - title: video.title, - id: video.id, - author: video.author, - ucid: video.ucid, - length_seconds: video.length_seconds, - published: video.published, - plid: playlist.id, - live_now: video.live_now, - index: Random::Secure.rand(0_i64..Int64::MAX), - }) - - Invidious::Database::PlaylistVideos.insert(playlist_video) - Invidious::Database::Playlists.update_video_added(playlist.id, playlist_video.index) - end - - videos = get_playlist_videos(playlist, offset: offset) - end - - return videos - end - # ------------------- # Invidious # ------------------- @@ -218,20 +215,20 @@ struct Invidious::User return true end - # Import playlist from Youtube - # Returns success status - #NEW + # Gavin Johnson (thtmnisamnstr), 20230127: Import playlist from Youtube export. Returns success status. def from_youtube_pl(user : User, body : String, filename : String, type : String) : Bool extension = filename.split(".").last if extension == "csv" || type == "text/csv" playlist = parse_playlist_export_csv(user, body) - playlist = parse_playlist_videos_export_csv(playlist, body) + if playlist + return true + else + return false + end else return false end - - return true end # ------------------- diff --git a/src/invidious/views/feeds/playlists.ecr b/src/invidious/views/feeds/playlists.ecr index a59344c4..05a48ce3 100644 --- a/src/invidious/views/feeds/playlists.ecr +++ b/src/invidious/views/feeds/playlists.ecr @@ -5,14 +5,21 @@ <%= rendered "components/feed_menu" %>
-
+

<%= translate(locale, "user_created_playlists", %(#{items_created.size})) %>

-
diff --git a/src/invidious/views/user/data_control.ecr b/src/invidious/views/user/data_control.ecr index 0f8e8dae..27654b40 100644 --- a/src/invidious/views/user/data_control.ecr +++ b/src/invidious/views/user/data_control.ecr @@ -8,7 +8,7 @@ <%= translate(locale, "Import") %>
- +
@@ -22,7 +22,7 @@
- +
From 5c7bda66ae90f3aef559a0269e56156a359814a3 Mon Sep 17 00:00:00 2001 From: Gavin Johnson Date: Sat, 28 Jan 2023 09:55:36 -0800 Subject: [PATCH 03/41] removed comments Signed-off-by: Gavin Johnson --- src/invidious/user/imports.cr | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/invidious/user/imports.cr b/src/invidious/user/imports.cr index fa1bbe7f..77009538 100644 --- a/src/invidious/user/imports.cr +++ b/src/invidious/user/imports.cr @@ -30,7 +30,6 @@ struct Invidious::User return subscriptions end - # Gavin Johnson (thtmnisamnstr), 20230127: Parse a youtube CSV playlist file and create the playlist def parse_playlist_export_csv(user : User, csv_content : String) rows = CSV.new(csv_content, headers: true) row_counter = 0 @@ -215,7 +214,6 @@ struct Invidious::User return true end - # Gavin Johnson (thtmnisamnstr), 20230127: Import playlist from Youtube export. Returns success status. def from_youtube_pl(user : User, body : String, filename : String, type : String) : Bool extension = filename.split(".").last From 72d0c9e40971b5460048f8906914fbef55289236 Mon Sep 17 00:00:00 2001 From: Gavin Johnson Date: Sat, 28 Jan 2023 09:57:28 -0800 Subject: [PATCH 04/41] removed comments Signed-off-by: Gavin Johnson --- src/invidious/routes/preferences.cr | 1 - 1 file changed, 1 deletion(-) diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr index adac0068..abe0f34e 100644 --- a/src/invidious/routes/preferences.cr +++ b/src/invidious/routes/preferences.cr @@ -310,7 +310,6 @@ module Invidious::Routes::PreferencesRoute response: error_template(415, "Invalid subscription file uploaded") ) end - # Gavin Johnson (thtmnisamnstr), 20230127: Call the Youtube playlist import function when "import_youtube_pl" filename = part.filename || "" success = Invidious::User::Import.from_youtube_pl(user, body, filename, type) From 6f01d6eacf0719e8569a338e5a44615f159c5120 Mon Sep 17 00:00:00 2001 From: thtmnisamnstr Date: Fri, 10 Feb 2023 12:00:02 -0800 Subject: [PATCH 05/41] ran crystal tool format. it should fix some CI issues Signed-off-by: thtmnisamnstr --- src/invidious/user/imports.cr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/invidious/user/imports.cr b/src/invidious/user/imports.cr index 77009538..aa87ca99 100644 --- a/src/invidious/user/imports.cr +++ b/src/invidious/user/imports.cr @@ -48,11 +48,11 @@ struct Invidious::User else privacy = PlaylistPrivacy::Private end - + if title && privacy && user - playlist = create_playlist(title, privacy, user) + playlist = create_playlist(title, privacy, user) end - + if playlist && description Invidious::Database::Playlists.update_description(playlist.id, description) end From b3eea6ab3ebdb1618916b02041b22e0e238e8a7d Mon Sep 17 00:00:00 2001 From: thtmnisamnstr Date: Thu, 23 Feb 2023 15:55:38 -0800 Subject: [PATCH 06/41] improved import algorithm, fixed a referer issue from the playlists page after deleting a playlist Signed-off-by: thtmnisamnstr --- src/invidious/user/imports.cr | 55 +++++++++++++------------ src/invidious/views/feeds/playlists.ecr | 4 +- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/src/invidious/user/imports.cr b/src/invidious/user/imports.cr index aa87ca99..7fddcc4c 100644 --- a/src/invidious/user/imports.cr +++ b/src/invidious/user/imports.cr @@ -30,43 +30,44 @@ struct Invidious::User return subscriptions end - def parse_playlist_export_csv(user : User, csv_content : String) - rows = CSV.new(csv_content, headers: true) - row_counter = 0 + def parse_playlist_export_csv(user : User, raw_input : String) playlist = uninitialized InvidiousPlaylist title = uninitialized String description = uninitialized String visibility = uninitialized String - rows.each do |row| - if row_counter == 0 - title = row[4] - description = row[5] - visibility = row[6] + privacy = uninitialized PlaylistPrivacy - if visibility.compare("Public", case_insensitive: true) == 0 - privacy = PlaylistPrivacy::Public - else - privacy = PlaylistPrivacy::Private - end + # Split the input into head and body content + raw_head, raw_body = raw_input.split("\n\n", limit: 2, remove_empty: true) - if title && privacy && user - playlist = create_playlist(title, privacy, user) - end + # Create the playlist from the head content + csv_head = CSV.new(raw_head, headers: true) + csv_head.next + title = csv_head[4] + description = csv_head[5] + visibility = csv_head[6] + + if visibility.compare("Public", case_insensitive: true) == 0 + privacy = PlaylistPrivacy::Public + else + privacy = PlaylistPrivacy::Private + end - if playlist && description - Invidious::Database::Playlists.update_description(playlist.id, description) - end + if title && privacy && user + playlist = create_playlist(title, privacy, user) + end - row_counter += 1 - end - if row_counter > 0 && row_counter < 3 - row_counter += 1 - end - if row_counter >= 3 + if playlist && description + Invidious::Database::Playlists.update_description(playlist.id, description) + end + + # Add each video to the playlist from the body content + CSV.each_row(raw_body) do |row| + if row.size >= 1 + video_id = row[0] if playlist - video_id = row[0] - row_counter += 1 next if !video_id + next if video_id == "Video Id" begin video = get_video(video_id) diff --git a/src/invidious/views/feeds/playlists.ecr b/src/invidious/views/feeds/playlists.ecr index 05a48ce3..43173355 100644 --- a/src/invidious/views/feeds/playlists.ecr +++ b/src/invidious/views/feeds/playlists.ecr @@ -10,12 +10,12 @@

- + "> <%= translate(locale, "Import/export") %>

From 3341929060bb20c086974a282fd1e33029f685f3 Mon Sep 17 00:00:00 2001 From: thtmnisamnstr Date: Tue, 7 Mar 2023 15:46:36 -0800 Subject: [PATCH 07/41] removed unnecessary conditionals and uninitialized variable declarations Signed-off-by: thtmnisamnstr --- src/invidious/user/imports.cr | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/invidious/user/imports.cr b/src/invidious/user/imports.cr index 7fddcc4c..757f5b13 100644 --- a/src/invidious/user/imports.cr +++ b/src/invidious/user/imports.cr @@ -31,12 +31,6 @@ struct Invidious::User end def parse_playlist_export_csv(user : User, raw_input : String) - playlist = uninitialized InvidiousPlaylist - title = uninitialized String - description = uninitialized String - visibility = uninitialized String - privacy = uninitialized PlaylistPrivacy - # Split the input into head and body content raw_head, raw_body = raw_input.split("\n\n", limit: 2, remove_empty: true) @@ -53,13 +47,8 @@ struct Invidious::User privacy = PlaylistPrivacy::Private end - if title && privacy && user - playlist = create_playlist(title, privacy, user) - end - - if playlist && description - Invidious::Database::Playlists.update_description(playlist.id, description) - end + playlist = create_playlist(title, privacy, user) + Invidious::Database::Playlists.update_description(playlist.id, description) # Add each video to the playlist from the body content CSV.each_row(raw_body) do |row| From fffdaa1410db7f9b5c67b1b47d401a2744e7b220 Mon Sep 17 00:00:00 2001 From: thtmnisamnstr Date: Mon, 3 Apr 2023 17:07:58 -0700 Subject: [PATCH 08/41] Updated csv reading as per feedback and ran Signed-off-by: thtmnisamnstr --- src/invidious/user/imports.cr | 53 +++++++++++++++++------------------ 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/src/invidious/user/imports.cr b/src/invidious/user/imports.cr index 757f5b13..673991f7 100644 --- a/src/invidious/user/imports.cr +++ b/src/invidious/user/imports.cr @@ -40,7 +40,7 @@ struct Invidious::User title = csv_head[4] description = csv_head[5] visibility = csv_head[6] - + if visibility.compare("Public", case_insensitive: true) == 0 privacy = PlaylistPrivacy::Public else @@ -51,34 +51,33 @@ struct Invidious::User Invidious::Database::Playlists.update_description(playlist.id, description) # Add each video to the playlist from the body content - CSV.each_row(raw_body) do |row| - if row.size >= 1 - video_id = row[0] - if playlist - next if !video_id - next if video_id == "Video Id" + csv_body = CSV.new(raw_body, headers: true) + csv_body.each do |row| + video_id = row[0] + if playlist + next if !video_id + next if video_id == "Video Id" - begin - video = get_video(video_id) - rescue ex - next - end - - playlist_video = PlaylistVideo.new({ - title: video.title, - id: video.id, - author: video.author, - ucid: video.ucid, - length_seconds: video.length_seconds, - published: video.published, - plid: playlist.id, - live_now: video.live_now, - index: Random::Secure.rand(0_i64..Int64::MAX), - }) - - Invidious::Database::PlaylistVideos.insert(playlist_video) - Invidious::Database::Playlists.update_video_added(playlist.id, playlist_video.index) + begin + video = get_video(video_id) + rescue ex + next end + + playlist_video = PlaylistVideo.new({ + title: video.title, + id: video.id, + author: video.author, + ucid: video.ucid, + length_seconds: video.length_seconds, + published: video.published, + plid: playlist.id, + live_now: video.live_now, + index: Random::Secure.rand(0_i64..Int64::MAX), + }) + + Invidious::Database::PlaylistVideos.insert(playlist_video) + Invidious::Database::Playlists.update_video_added(playlist.id, playlist_video.index) end end From d6fb5c03b72b40bf7bd71f8023c71c76ea41f53d Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Thu, 16 Mar 2023 11:03:07 -0400 Subject: [PATCH 09/41] add hashtag endpoint --- src/invidious/routes/api/v1/search.cr | 30 +++++++++++++++++++++++++++ src/invidious/routing.cr | 1 + 2 files changed, 31 insertions(+) diff --git a/src/invidious/routes/api/v1/search.cr b/src/invidious/routes/api/v1/search.cr index 21451d33..0bf74bc3 100644 --- a/src/invidious/routes/api/v1/search.cr +++ b/src/invidious/routes/api/v1/search.cr @@ -55,4 +55,34 @@ module Invidious::Routes::API::V1::Search return error_json(500, ex) end end + + def self.hashtag(env) + hashtag = env.params.url["hashtag"] + + # page does not change anything. + # page = env.params.query["page"]?.try &.to_i?|| 1 + + page = 1 + locale = env.get("preferences").as(Preferences).locale + region = env.params.query["region"]? + env.response.content_type = "application/json" + + begin + results = Invidious::Hashtag.fetch(hashtag, page, region) + rescue ex + return error_json(400, ex) + end + + JSON.build do |json| + json.object do + json.field "results" do + json.array do + results.each do |item| + item.to_json(locale, json) + end + end + end + end + end + end end diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index 9e2ade3d..72ee9194 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -243,6 +243,7 @@ module Invidious::Routing # Search get "/api/v1/search", {{namespace}}::Search, :search get "/api/v1/search/suggestions", {{namespace}}::Search, :search_suggestions + get "/api/v1/hashtag/:hashtag", {{namespace}}::Search, :hashtag # Authenticated From d7285992517c98a276e325f83e1b7584dac3c498 Mon Sep 17 00:00:00 2001 From: chunky programmer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Sun, 14 May 2023 15:20:59 -0400 Subject: [PATCH 10/41] add page parameter --- src/invidious/routes/api/v1/search.cr | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/invidious/routes/api/v1/search.cr b/src/invidious/routes/api/v1/search.cr index 0bf74bc3..9fb283c2 100644 --- a/src/invidious/routes/api/v1/search.cr +++ b/src/invidious/routes/api/v1/search.cr @@ -59,10 +59,8 @@ module Invidious::Routes::API::V1::Search def self.hashtag(env) hashtag = env.params.url["hashtag"] - # page does not change anything. - # page = env.params.query["page"]?.try &.to_i?|| 1 + page = env.params.query["page"]?.try &.to_i? || 1 - page = 1 locale = env.get("preferences").as(Preferences).locale region = env.params.query["region"]? env.response.content_type = "application/json" From 12b4dd9191307c2b3387a4c73c2fc06be5da7703 Mon Sep 17 00:00:00 2001 From: chunky programmer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Sun, 14 May 2023 17:25:32 -0400 Subject: [PATCH 11/41] Populate search bar with ChannelId --- src/invidious/routes/channels.cr | 1 + src/invidious/routes/search.cr | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index d3969d29..740f3096 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -278,6 +278,7 @@ module Invidious::Routes::Channels return error_template(500, ex) end + env.set "search", "channel:" + ucid + " " return {locale, user, subscriptions, continuation, ucid, channel} end end diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index 2a9705cf..7f17124e 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -65,7 +65,11 @@ module Invidious::Routes::Search redirect_url = Invidious::Frontend::Misc.redirect_url(env) - env.set "search", query.text + if query.type == Invidious::Search::Query::Type::Channel + env.set "search", "channel:" + query.channel + " " + query.text + else + env.set "search", query.text + end templated "search" end end From 8bd2e60abc42f51e6cdd246e883ab953cabd78ae Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Mon, 22 May 2023 09:19:32 -0400 Subject: [PATCH 12/41] Use string interpolation instead of concatenation Co-authored-by: Samantaz Fox --- src/invidious/routes/channels.cr | 2 +- src/invidious/routes/search.cr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index 740f3096..16621994 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -278,7 +278,7 @@ module Invidious::Routes::Channels return error_template(500, ex) end - env.set "search", "channel:" + ucid + " " + env.set "search", "channel:#{ucid} " return {locale, user, subscriptions, continuation, ucid, channel} end end diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index 7f17124e..6c3088de 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -66,7 +66,7 @@ module Invidious::Routes::Search redirect_url = Invidious::Frontend::Misc.redirect_url(env) if query.type == Invidious::Search::Query::Type::Channel - env.set "search", "channel:" + query.channel + " " + query.text + env.set "search", "channel:#{query.channel} #{query.text}" else env.set "search", query.text end From 6440ae0b5c15355dd87959412ea609396a198215 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Tue, 9 May 2023 23:37:49 +0200 Subject: [PATCH 13/41] Community: Fix position of the "creator heart" (broken by #3783) --- assets/css/default.css | 2 ++ src/invidious/comments.cr | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index 23649f8f..431a0427 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -46,6 +46,7 @@ body a.channel-owner { } .creator-heart { + display: inline-block; position: relative; width: 16px; height: 16px; @@ -66,6 +67,7 @@ body a.channel-owner { } .creator-heart-small-container { + display: block; position: relative; width: 13px; height: 13px; diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 01556099..466c9fe5 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -409,7 +409,6 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false) html << <<-END_HTML #{number_with_separator(child["likeCount"])} -

END_HTML if child["creatorHeart"]? @@ -420,13 +419,14 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false) end html << <<-END_HTML +   -
+ -
-
-
-
+ + + +
END_HTML end From ef4ff4e4b25855379bae367f96e40a267b227f83 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Tue, 9 May 2023 10:20:27 +0000 Subject: [PATCH 14/41] Update Spanish translation --- locales/es.json | 80 +++++++++++++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/locales/es.json b/locales/es.json index 68ff0170..74f80a64 100644 --- a/locales/es.json +++ b/locales/es.json @@ -398,36 +398,51 @@ "search_filters_features_option_three_sixty": "360°", "videoinfo_watch_on_youTube": "Ver en YouTube", "preferences_save_player_pos_label": "Guardar posición de reproducción: ", - "generic_views_count": "{{count}} vista", - "generic_views_count_plural": "{{count}} vistas", - "generic_subscribers_count": "{{count}} suscriptor", - "generic_subscribers_count_plural": "{{count}} suscriptores", - "generic_subscriptions_count": "{{count}} suscripción", - "generic_subscriptions_count_plural": "{{count}} suscripciones", - "subscriptions_unseen_notifs_count": "{{count}} notificación sin ver", - "subscriptions_unseen_notifs_count_plural": "{{count}} notificaciones sin ver", - "generic_count_days": "{{count}} día", - "generic_count_days_plural": "{{count}} días", - "comments_view_x_replies": "Ver {{count}} respuesta", - "comments_view_x_replies_plural": "Ver {{count}} respuestas", - "generic_count_weeks": "{{count}} semana", - "generic_count_weeks_plural": "{{count}} semanas", - "generic_playlists_count": "{{count}} reproducción", - "generic_playlists_count_plural": "{{count}} reproducciones", - "generic_videos_count": "{{count}} video", - "generic_videos_count_plural": "{{count}} videos", - "generic_count_months": "{{count}} mes", - "generic_count_months_plural": "{{count}} meses", - "comments_points_count": "{{count}} punto", - "comments_points_count_plural": "{{count}} puntos", - "generic_count_years": "{{count}} año", - "generic_count_years_plural": "{{count}} años", - "generic_count_hours": "{{count}} hora", - "generic_count_hours_plural": "{{count}} horas", - "generic_count_minutes": "{{count}} minuto", - "generic_count_minutes_plural": "{{count}} minutos", - "generic_count_seconds": "{{count}} segundo", - "generic_count_seconds_plural": "{{count}} segundos", + "generic_views_count_0": "{{count}} vista", + "generic_views_count_1": "{{count}} vistas", + "generic_views_count_2": "{{count}} vistas", + "generic_subscribers_count_0": "{{count}} suscriptor", + "generic_subscribers_count_1": "{{count}} suscriptores", + "generic_subscribers_count_2": "{{count}} suscriptores", + "generic_subscriptions_count_0": "{{count}} suscripción", + "generic_subscriptions_count_1": "{{count}} suscripciones", + "generic_subscriptions_count_2": "{{count}} suscripciones", + "subscriptions_unseen_notifs_count_0": "{{count}} notificación no vista", + "subscriptions_unseen_notifs_count_1": "{{count}} notificaciones no vistas", + "subscriptions_unseen_notifs_count_2": "{{count}} notificaciones no vistas", + "generic_count_days_0": "{{count}} día", + "generic_count_days_1": "{{count}} días", + "generic_count_days_2": "{{count}} días", + "comments_view_x_replies_0": "Ver {{count}} respuesta", + "comments_view_x_replies_1": "Ver las {{count}} respuestas", + "comments_view_x_replies_2": "Ver las {{count}} respuestas", + "generic_count_weeks_0": "{{count}} semana", + "generic_count_weeks_1": "{{count}} semanas", + "generic_count_weeks_2": "{{count}} semanas", + "generic_playlists_count_0": "{{count}} lista de reproducción", + "generic_playlists_count_1": "{{count}} listas de reproducciones", + "generic_playlists_count_2": "{{count}} listas de reproducciones", + "generic_videos_count_0": "{{count}} video", + "generic_videos_count_1": "{{count}} videos", + "generic_videos_count_2": "{{count}} videos", + "generic_count_months_0": "{{count}} mes", + "generic_count_months_1": "{{count}} meses", + "generic_count_months_2": "{{count}} meses", + "comments_points_count_0": "{{count}} punto", + "comments_points_count_1": "{{count}} puntos", + "comments_points_count_2": "{{count}} puntos", + "generic_count_years_0": "{{count}} año", + "generic_count_years_1": "{{count}} años", + "generic_count_years_2": "{{count}} años", + "generic_count_hours_0": "{{count}} hora", + "generic_count_hours_1": "{{count}} horas", + "generic_count_hours_2": "{{count}} horas", + "generic_count_minutes_0": "{{count}} minuto", + "generic_count_minutes_1": "{{count}} minutos", + "generic_count_minutes_2": "{{count}} minutos", + "generic_count_seconds_0": "{{count}} segundo", + "generic_count_seconds_1": "{{count}} segundos", + "generic_count_seconds_2": "{{count}} segundos", "crash_page_before_reporting": "Antes de notificar un error asegúrate de que has:", "crash_page_switch_instance": "probado a usar otra instancia", "crash_page_read_the_faq": "leído las Preguntas Frecuentes", @@ -468,8 +483,9 @@ "search_filters_duration_option_none": "Cualquier duración", "search_filters_features_option_vr180": "VR180", "search_filters_apply_button": "Aplicar filtros", - "tokens_count": "{{count}} token", - "tokens_count_plural": "{{count}} tokens", + "tokens_count_0": "{{count}} token", + "tokens_count_1": "{{count}} tokens", + "tokens_count_2": "{{count}} tokens", "search_message_use_another_instance": " También puede buscar en otra instancia.", "Popular enabled: ": "¿Habilitar la sección popular? ", "error_video_not_in_playlist": "El video que solicitaste no existe en esta lista de reproducción. Haz clic aquí para acceder a la página de inicio de la lista de reproducción.", From a79b7ef170f8806c25f43b0fa5deaaa7e27eb53d Mon Sep 17 00:00:00 2001 From: maboroshin Date: Wed, 10 May 2023 11:40:49 +0000 Subject: [PATCH 15/41] Update Japanese translation --- locales/ja.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/locales/ja.json b/locales/ja.json index d1813bcd..49763cc8 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -314,7 +314,7 @@ "Zulu": "ズール語", "generic_count_years_0": "{{count}}年", "generic_count_months_0": "{{count}}か月", - "generic_count_weeks_0": "{{count}}週", + "generic_count_weeks_0": "{{count}}週間", "generic_count_days_0": "{{count}}日", "generic_count_hours_0": "{{count}}時間", "generic_count_minutes_0": "{{count}}分", @@ -442,7 +442,7 @@ "crash_page_switch_instance": "別のインスタンスを使用を試す", "crash_page_read_the_faq": "よくある質問 (FAQ) を読む", "Popular enabled: ": "人気動画を有効化 ", - "search_message_use_another_instance": " 別のインスタンス上でも検索できます。", + "search_message_use_another_instance": " 別のインスタンス上での検索も可能です。", "search_filters_apply_button": "選択したフィルターを適用", "user_saved_playlists": "`x` 個の保存した再生リスト", "crash_page_you_found_a_bug": "Invidious のバグのようです!", @@ -466,5 +466,6 @@ "Album: ": "アルバム: ", "Song: ": "曲: ", "Channel Sponsor": "チャンネルのスポンサー", - "Standard YouTube license": "標準 Youtube ライセンス" + "Standard YouTube license": "標準 Youtube ライセンス", + "Download is disabled": "ダウンロード: このインスタンスでは未対応" } From e65671454287e0aabf1bdb4619f9a352d56d12e0 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 17 May 2023 16:34:16 +0000 Subject: [PATCH 16/41] Update German translation --- locales/de.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 0df86663..1a6f2cea 100644 --- a/locales/de.json +++ b/locales/de.json @@ -482,5 +482,6 @@ "channel_tab_channels_label": "Kanäle", "Channel Sponsor": "Kanalsponsor", "Standard YouTube license": "Standard YouTube-Lizenz", - "Song: ": "Musik: " + "Song: ": "Musik: ", + "Download is disabled": "Herunterladen ist deaktiviert" } From f2cc97b2902dc647c0deb97e0a554e41ef2c2cce Mon Sep 17 00:00:00 2001 From: joaooliva Date: Sat, 20 May 2023 14:27:29 +0000 Subject: [PATCH 17/41] Update Portuguese (Brazil) translation --- locales/pt-BR.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/locales/pt-BR.json b/locales/pt-BR.json index ec00d46e..759aec94 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -479,5 +479,9 @@ "channel_tab_streams_label": "Ao Vivo", "Music in this video": "Música neste vídeo", "Artist: ": "Artista: ", - "Album: ": "Álbum: " + "Album: ": "Álbum: ", + "Standard YouTube license": "Licença padrão do YouTube", + "Song: ": "Música: ", + "Channel Sponsor": "Patrocinador do Canal", + "Download is disabled": "Download está desativado" } From 11d45adcdcd20e1a0c0f2c403e59e2d2ff801388 Mon Sep 17 00:00:00 2001 From: Ashirg-ch Date: Tue, 23 May 2023 08:03:47 +0000 Subject: [PATCH 18/41] Update German translation --- locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 1a6f2cea..3c1120c0 100644 --- a/locales/de.json +++ b/locales/de.json @@ -433,7 +433,7 @@ "comments_points_count_plural": "{{count}} Punkte", "crash_page_you_found_a_bug": "Anscheinend haben Sie einen Fehler in Invidious gefunden!", "generic_count_months": "{{count}} Monat", - "generic_count_months_plural": "{{count}} Monate", + "generic_count_months_plural": "{{count}} Monaten", "Cantonese (Hong Kong)": "Kantonesisch (Hong Kong)", "Chinese (Hong Kong)": "Chinesisch (Hong Kong)", "generic_playlists_count": "{{count}} Wiedergabeliste", From 67a79faaeb0f87b93f1247f8c87ff9aba5018b22 Mon Sep 17 00:00:00 2001 From: Matthaiks Date: Tue, 23 May 2023 20:15:19 +0000 Subject: [PATCH 19/41] Update Polish translation --- locales/pl.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/pl.json b/locales/pl.json index 2b6768d9..ca80757c 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -499,5 +499,6 @@ "Album: ": "Album: ", "Song: ": "Piosenka: ", "Channel Sponsor": "Sponsor kanału", - "Standard YouTube license": "Standardowa licencja YouTube" + "Standard YouTube license": "Standardowa licencja YouTube", + "Import YouTube playlist (.csv)": "Importuj playlistę YouTube (.csv)" } From 7e3c685cd619cc65ae6b7e34c22de8471dc1bd00 Mon Sep 17 00:00:00 2001 From: Rex_sa Date: Thu, 25 May 2023 06:12:01 +0000 Subject: [PATCH 20/41] Update Arabic translation --- locales/ar.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/ar.json b/locales/ar.json index 7303915b..6fe5b8bf 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -547,5 +547,6 @@ "Song: ": "أغنية: ", "Channel Sponsor": "راعي القناة", "Standard YouTube license": "ترخيص YouTube القياسي", - "Download is disabled": "تم تعطيل التحميلات" + "Download is disabled": "تم تعطيل التحميلات", + "Import YouTube playlist (.csv)": "استيراد قائمة تشغيل YouTube (.csv)" } From f0120bece165f3d325b758e3b1d837b084f0dd62 Mon Sep 17 00:00:00 2001 From: atilluF <110931720+atilluF@users.noreply.github.com> Date: Thu, 25 May 2023 15:26:00 +0000 Subject: [PATCH 21/41] Update Italian translation --- locales/it.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/it.json b/locales/it.json index 0797b387..9299add7 100644 --- a/locales/it.json +++ b/locales/it.json @@ -483,5 +483,6 @@ "Download is disabled": "Il download è disabilitato", "Song: ": "Canzone: ", "Standard YouTube license": "Licenza standard di YouTube", - "Channel Sponsor": "Sponsor del canale" + "Channel Sponsor": "Sponsor del canale", + "Import YouTube playlist (.csv)": "Importa playlist di YouTube (.csv)" } From 184bd3204fe0122ab18296962de9ba976fb89ff2 Mon Sep 17 00:00:00 2001 From: Jorge Maldonado Ventura Date: Tue, 23 May 2023 20:16:45 +0000 Subject: [PATCH 22/41] Update Spanish translation --- locales/es.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/es.json b/locales/es.json index 74f80a64..4940dd90 100644 --- a/locales/es.json +++ b/locales/es.json @@ -499,5 +499,6 @@ "Song: ": "Canción: ", "Channel Sponsor": "Patrocinador del canal", "Standard YouTube license": "Licencia de YouTube estándar", - "Download is disabled": "La descarga está deshabilitada" + "Download is disabled": "La descarga está deshabilitada", + "Import YouTube playlist (.csv)": "Importar lista de reproducción de YouTube (.csv)" } From ea6db9c58ab74c248cda7dfa8ec2e68f1868b49f Mon Sep 17 00:00:00 2001 From: Jorge Maldonado Ventura Date: Tue, 23 May 2023 20:16:16 +0000 Subject: [PATCH 23/41] Update Esperanto translation --- locales/eo.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/eo.json b/locales/eo.json index 464d16ca..4e789390 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -483,5 +483,6 @@ "Channel Sponsor": "Kanala sponsoro", "Song: ": "Muzikaĵo: ", "Standard YouTube license": "Implicita YouTube-licenco", - "Download is disabled": "Elŝuto estas malebligita" + "Download is disabled": "Elŝuto estas malebligita", + "Import YouTube playlist (.csv)": "Importi YouTube-ludliston (.csv)" } From fd06656d86a68d226458e2648cded2603fa07bbf Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Tue, 23 May 2023 21:50:59 +0000 Subject: [PATCH 24/41] Update Ukrainian translation --- locales/uk.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/uk.json b/locales/uk.json index 61bf3d31..863916f7 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -499,5 +499,6 @@ "Song: ": "Пісня: ", "Channel Sponsor": "Спонсор каналу", "Standard YouTube license": "Стандартна ліцензія YouTube", - "Download is disabled": "Завантаження вимкнено" + "Download is disabled": "Завантаження вимкнено", + "Import YouTube playlist (.csv)": "Імпорт списку відтворення YouTube (.csv)" } From e8df08e41eedfd26364110562f134735e307d7b6 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 25 May 2023 07:58:41 +0000 Subject: [PATCH 25/41] Update Chinese (Simplified) translation --- locales/zh-CN.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/zh-CN.json b/locales/zh-CN.json index df31812a..fdd940c3 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -467,5 +467,6 @@ "Song: ": "歌曲: ", "Channel Sponsor": "频道赞助者", "Standard YouTube license": "标准 YouTube 许可证", - "Download is disabled": "已禁用下载" + "Download is disabled": "已禁用下载", + "Import YouTube playlist (.csv)": "导入 YouTube 播放列表(.csv)" } From f0f6cb0d83545d852dddac94b9d7ce7bf666db60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuz=20Ersen?= Date: Wed, 24 May 2023 17:45:42 +0000 Subject: [PATCH 26/41] Update Turkish translation --- locales/tr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/tr.json b/locales/tr.json index a2fdd573..ca74ef23 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -483,5 +483,6 @@ "Channel Sponsor": "Kanal Sponsoru", "Song: ": "Şarkı: ", "Standard YouTube license": "Standart YouTube lisansı", - "Download is disabled": "İndirme devre dışı" + "Download is disabled": "İndirme devre dışı", + "Import YouTube playlist (.csv)": "YouTube Oynatma Listesini İçe Aktar (.csv)" } From a727bb037f4c75f6c30e0e52f050a6e6c8d4325f Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Thu, 25 May 2023 02:00:55 +0000 Subject: [PATCH 27/41] Update Chinese (Traditional) translation --- locales/zh-TW.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/zh-TW.json b/locales/zh-TW.json index daa22493..593a946a 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -467,5 +467,6 @@ "Channel Sponsor": "頻道贊助者", "Song: ": "歌曲: ", "Standard YouTube license": "標準 YouTube 授權條款", - "Download is disabled": "已停用下載" + "Download is disabled": "已停用下載", + "Import YouTube playlist (.csv)": "匯入 YouTube 播放清單 (.csv)" } From ed2d16c91d2b7a2de51e556bc7e360e18a58a854 Mon Sep 17 00:00:00 2001 From: maboroshin Date: Wed, 24 May 2023 13:27:34 +0000 Subject: [PATCH 28/41] Update Japanese translation --- locales/ja.json | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/locales/ja.json b/locales/ja.json index 49763cc8..d9207d3f 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -8,8 +8,8 @@ "Shared `x` ago": "`x`前に公開", "Unsubscribe": "登録解除", "Subscribe": "登録", - "View channel on YouTube": "YouTube でチャンネルを見る", - "View playlist on YouTube": "YouTube で再生リストを見る", + "View channel on YouTube": "YouTube でチャンネルを表示", + "View playlist on YouTube": "YouTube で再生リストを表示", "newest": "新しい順", "oldest": "古い順", "popular": "人気順", @@ -69,7 +69,7 @@ "preferences_captions_label": "優先する字幕: ", "Fallback captions: ": "フォールバック時の字幕: ", "preferences_related_videos_label": "関連動画を表示: ", - "preferences_annotations_label": "デフォルトでアノテーションを表示: ", + "preferences_annotations_label": "最初からアノテーションを表示: ", "preferences_extend_desc_label": "動画の説明文を自動的に拡張: ", "preferences_vr_mode_label": "対話的な360°動画 (WebGL が必要): ", "preferences_category_visual": "外観設定", @@ -82,7 +82,7 @@ "preferences_category_misc": "ほかの設定", "preferences_automatic_instance_redirect_label": "インスタンスの自動転送 (redirect.invidious.ioにフォールバック): ", "preferences_category_subscription": "登録チャンネル設定", - "preferences_annotations_subscribed_label": "デフォルトで登録チャンネルのアノテーションを表示しますか? ", + "preferences_annotations_subscribed_label": "最初から登録チャンネルのアノテーションを表示 ", "Redirect homepage to feed: ": "ホームからフィードにリダイレクト: ", "preferences_max_results_label": "フィードに表示する動画の量: ", "preferences_sort_label": "動画を並び替え: ", @@ -110,7 +110,7 @@ "preferences_category_admin": "管理者設定", "preferences_default_home_label": "ホームに表示するページ: ", "preferences_feed_menu_label": "フィードメニュー: ", - "preferences_show_nick_label": "ニックネームを一番上に表示する: ", + "preferences_show_nick_label": "ログイン名を上部に表示: ", "Top enabled: ": "トップページを有効化: ", "CAPTCHA enabled: ": "CAPTCHA を有効化: ", "Login enabled: ": "ログインを有効化: ", @@ -131,7 +131,7 @@ "Released under the AGPLv3 on Github.": "GitHub 上で AGPLv3 の元で公開", "Source available here.": "ソースはここで閲覧可能です。", "View JavaScript license information.": "JavaScript ライセンス情報", - "View privacy policy.": "プライバシーポリシー", + "View privacy policy.": "個人情報保護方針", "Trending": "急上昇", "Public": "公開", "Unlisted": "限定公開", @@ -142,11 +142,11 @@ "Delete playlist": "再生リストを削除", "Create playlist": "再生リストを作成", "Title": "タイトル", - "Playlist privacy": "再生リストの公開設定", + "Playlist privacy": "再生リストの公開状態", "Editing playlist `x`": "再生リスト `x` を編集中", "Show more": "もっと見る", "Show less": "表示を少なく", - "Watch on YouTube": "YouTube で視聴", + "Watch on YouTube": "YouTubeで視聴", "Switch Invidious Instance": "Invidious インスタンスの変更", "Hide annotations": "アノテーションを隠す", "Show annotations": "アノテーションを表示", @@ -161,13 +161,13 @@ "Premieres in `x`": "`x`後にプレミア公開", "Premieres `x`": "`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.": "やあ!君は JavaScript を無効にしているのかな?ここをクリックしてコメントを見れるけど、読み込みには少し時間がかかることがあるのを覚えておいてね。", - "View YouTube comments": "YouTube のコメントを見る", + "View YouTube comments": "YouTube のコメントを表示", "View more comments on Reddit": "Reddit でコメントをもっと見る", "View `x` comments": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 件のコメントを見る", - "": "`x` 件のコメントを見る" + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 件のコメントを表示", + "": "`x` 件のコメントを表示" }, - "View Reddit comments": "Reddit のコメントを見る", + "View Reddit comments": "Reddit のコメントを表示", "Hide replies": "返信を非表示", "Show replies": "返信を表示", "Incorrect password": "パスワードが間違っています", @@ -326,8 +326,8 @@ "About": "このサービスについて", "Rating: ": "評価: ", "preferences_locale_label": "言語: ", - "View as playlist": "再生リストで見る", - "Default": "デフォルト", + "View as playlist": "再生リストとして閲覧", + "Default": "標準", "Music": "音楽", "Gaming": "ゲーム", "News": "ニュース", @@ -375,7 +375,7 @@ "next_steps_error_message_refresh": "再読込", "next_steps_error_message_go_to_youtube": "YouTubeへ", "search_filters_duration_option_short": "4 分未満", - "footer_documentation": "文書", + "footer_documentation": "説明書", "footer_source_code": "ソースコード", "footer_original_source_code": "元のソースコード", "footer_modfied_source_code": "改変して使用", @@ -407,7 +407,7 @@ "preferences_quality_dash_option_worst": "最悪", "preferences_quality_dash_option_best": "最高", "videoinfo_started_streaming_x_ago": "`x`前に配信を開始", - "videoinfo_watch_on_youTube": "YouTube上で見る", + "videoinfo_watch_on_youTube": "YouTubeで視聴", "user_created_playlists": "`x`個の作成した再生リスト", "Video unavailable": "動画は利用できません", "Chinese": "中国語", @@ -467,5 +467,6 @@ "Song: ": "曲: ", "Channel Sponsor": "チャンネルのスポンサー", "Standard YouTube license": "標準 Youtube ライセンス", - "Download is disabled": "ダウンロード: このインスタンスでは未対応" + "Download is disabled": "ダウンロード: このインスタンスでは未対応", + "Import YouTube playlist (.csv)": "YouTube 再生リストをインポート (.csv)" } From fe97b3d76185080cd63245b32f067ed5dad94a4c Mon Sep 17 00:00:00 2001 From: Milo Ivir Date: Tue, 23 May 2023 21:16:15 +0000 Subject: [PATCH 29/41] Update Croatian translation --- locales/hr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/hr.json b/locales/hr.json index b87a7729..46e07b83 100644 --- a/locales/hr.json +++ b/locales/hr.json @@ -499,5 +499,6 @@ "Channel Sponsor": "Sponzor kanala", "Song: ": "Pjesma: ", "Standard YouTube license": "Standardna YouTube licenca", - "Download is disabled": "Preuzimanje je deaktivirano" + "Download is disabled": "Preuzimanje je deaktivirano", + "Import YouTube playlist (.csv)": "Uvezi YouTube zbirku (.csv)" } From c9eafb250f108505b6aa4559f708ae45d3845df7 Mon Sep 17 00:00:00 2001 From: Fjuro Date: Wed, 24 May 2023 18:35:19 +0000 Subject: [PATCH 30/41] Update Czech translation --- locales/cs.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/cs.json b/locales/cs.json index d9e5b4d5..8e656827 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -499,5 +499,6 @@ "Channel Sponsor": "Sponzor kanálu", "Song: ": "Skladba: ", "Standard YouTube license": "Standardní licence YouTube", - "Download is disabled": "Stahování je zakázáno" + "Download is disabled": "Stahování je zakázáno", + "Import YouTube playlist (.csv)": "Importovat YouTube playlist (.csv)" } From 4b29f8254a412fc7a0f278a1677844627499e455 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 25 May 2023 22:44:08 +0200 Subject: [PATCH 31/41] Fix broken Spanish locale (i18next v3->v4 mixup) --- locales/es.json | 80 ++++++++++++++++++++----------------------------- 1 file changed, 32 insertions(+), 48 deletions(-) diff --git a/locales/es.json b/locales/es.json index 4940dd90..0425ed68 100644 --- a/locales/es.json +++ b/locales/es.json @@ -398,51 +398,36 @@ "search_filters_features_option_three_sixty": "360°", "videoinfo_watch_on_youTube": "Ver en YouTube", "preferences_save_player_pos_label": "Guardar posición de reproducción: ", - "generic_views_count_0": "{{count}} vista", - "generic_views_count_1": "{{count}} vistas", - "generic_views_count_2": "{{count}} vistas", - "generic_subscribers_count_0": "{{count}} suscriptor", - "generic_subscribers_count_1": "{{count}} suscriptores", - "generic_subscribers_count_2": "{{count}} suscriptores", - "generic_subscriptions_count_0": "{{count}} suscripción", - "generic_subscriptions_count_1": "{{count}} suscripciones", - "generic_subscriptions_count_2": "{{count}} suscripciones", - "subscriptions_unseen_notifs_count_0": "{{count}} notificación no vista", - "subscriptions_unseen_notifs_count_1": "{{count}} notificaciones no vistas", - "subscriptions_unseen_notifs_count_2": "{{count}} notificaciones no vistas", - "generic_count_days_0": "{{count}} día", - "generic_count_days_1": "{{count}} días", - "generic_count_days_2": "{{count}} días", - "comments_view_x_replies_0": "Ver {{count}} respuesta", - "comments_view_x_replies_1": "Ver las {{count}} respuestas", - "comments_view_x_replies_2": "Ver las {{count}} respuestas", - "generic_count_weeks_0": "{{count}} semana", - "generic_count_weeks_1": "{{count}} semanas", - "generic_count_weeks_2": "{{count}} semanas", - "generic_playlists_count_0": "{{count}} lista de reproducción", - "generic_playlists_count_1": "{{count}} listas de reproducciones", - "generic_playlists_count_2": "{{count}} listas de reproducciones", - "generic_videos_count_0": "{{count}} video", - "generic_videos_count_1": "{{count}} videos", - "generic_videos_count_2": "{{count}} videos", - "generic_count_months_0": "{{count}} mes", - "generic_count_months_1": "{{count}} meses", - "generic_count_months_2": "{{count}} meses", - "comments_points_count_0": "{{count}} punto", - "comments_points_count_1": "{{count}} puntos", - "comments_points_count_2": "{{count}} puntos", - "generic_count_years_0": "{{count}} año", - "generic_count_years_1": "{{count}} años", - "generic_count_years_2": "{{count}} años", - "generic_count_hours_0": "{{count}} hora", - "generic_count_hours_1": "{{count}} horas", - "generic_count_hours_2": "{{count}} horas", - "generic_count_minutes_0": "{{count}} minuto", - "generic_count_minutes_1": "{{count}} minutos", - "generic_count_minutes_2": "{{count}} minutos", - "generic_count_seconds_0": "{{count}} segundo", - "generic_count_seconds_1": "{{count}} segundos", - "generic_count_seconds_2": "{{count}} segundos", + "generic_views_count": "{{count}} vista", + "generic_views_count_plural": "{{count}} vistas", + "generic_subscribers_count": "{{count}} suscriptor", + "generic_subscribers_count_plural": "{{count}} suscriptores", + "generic_subscriptions_count": "{{count}} suscripción", + "generic_subscriptions_count_plural": "{{count}} suscripciones", + "subscriptions_unseen_notifs_count": "{{count}} notificación no vista", + "subscriptions_unseen_notifs_count_plural": "{{count}} notificaciones no vistas", + "generic_count_days": "{{count}} día", + "generic_count_days_plural": "{{count}} días", + "comments_view_x_replies": "Ver {{count}} respuesta", + "comments_view_x_replies_plural": "Ver {{count}} respuestas", + "generic_count_weeks": "{{count}} semana", + "generic_count_weeks_plural": "{{count}} semanas", + "generic_playlists_count": "{{count}} lista de reproducción", + "generic_playlists_count_plural": "{{count}} listas de reproducciones", + "generic_videos_count": "{{count}} video", + "generic_videos_count_plural": "{{count}} videos", + "generic_count_months": "{{count}} mes", + "generic_count_months_plural": "{{count}} meses", + "comments_points_count": "{{count}} punto", + "comments_points_count_plural": "{{count}} puntos", + "generic_count_years": "{{count}} año", + "generic_count_years_plural": "{{count}} años", + "generic_count_hours": "{{count}} hora", + "generic_count_hours_plural": "{{count}} horas", + "generic_count_minutes": "{{count}} minuto", + "generic_count_minutes_plural": "{{count}} minutos", + "generic_count_seconds": "{{count}} segundo", + "generic_count_seconds_plural": "{{count}} segundos", "crash_page_before_reporting": "Antes de notificar un error asegúrate de que has:", "crash_page_switch_instance": "probado a usar otra instancia", "crash_page_read_the_faq": "leído las Preguntas Frecuentes", @@ -483,9 +468,8 @@ "search_filters_duration_option_none": "Cualquier duración", "search_filters_features_option_vr180": "VR180", "search_filters_apply_button": "Aplicar filtros", - "tokens_count_0": "{{count}} token", - "tokens_count_1": "{{count}} tokens", - "tokens_count_2": "{{count}} tokens", + "tokens_count": "{{count}} token", + "tokens_count_plural": "{{count}} tokens", "search_message_use_another_instance": " También puede buscar en otra instancia.", "Popular enabled: ": "¿Habilitar la sección popular? ", "error_video_not_in_playlist": "El video que solicitaste no existe en esta lista de reproducción. Haz clic aquí para acceder a la página de inicio de la lista de reproducción.", From c7876d564f09995244186f57d61cedfeb63038b6 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 6 May 2023 19:50:35 +0200 Subject: [PATCH 32/41] Comments: add 'require' statement for a dedicated folder --- src/invidious.cr | 1 + 1 file changed, 1 insertion(+) diff --git a/src/invidious.cr b/src/invidious.cr index d4f8e0fb..b5abd5c7 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -43,6 +43,7 @@ require "./invidious/videos/*" require "./invidious/jsonify/**" require "./invidious/*" +require "./invidious/comments/*" require "./invidious/channels/*" require "./invidious/user/*" require "./invidious/search/*" From 8dd18248692726e8db05138c4ce2b01f39ad62f6 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 6 May 2023 19:51:49 +0200 Subject: [PATCH 33/41] Comments: Move reddit type definitions to their own file --- src/invidious/comments.cr | 58 -------------------------- src/invidious/comments/reddit_types.cr | 57 +++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 58 deletions(-) create mode 100644 src/invidious/comments/reddit_types.cr diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 466c9fe5..00e8d399 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -1,61 +1,3 @@ -class RedditThing - include JSON::Serializable - - property kind : String - property data : RedditComment | RedditLink | RedditMore | RedditListing -end - -class RedditComment - include JSON::Serializable - - property author : String - property body_html : String - property replies : RedditThing | String - property score : Int32 - property depth : Int32 - property permalink : String - - @[JSON::Field(converter: RedditComment::TimeConverter)] - property created_utc : Time - - module TimeConverter - def self.from_json(value : JSON::PullParser) : Time - Time.unix(value.read_float.to_i) - end - - def self.to_json(value : Time, json : JSON::Builder) - json.number(value.to_unix) - end - end -end - -struct RedditLink - include JSON::Serializable - - property author : String - property score : Int32 - property subreddit : String - property num_comments : Int32 - property id : String - property permalink : String - property title : String -end - -struct RedditMore - include JSON::Serializable - - property children : Array(String) - property count : Int32 - property depth : Int32 -end - -class RedditListing - include JSON::Serializable - - property children : Array(RedditThing) - property modhash : String -end - def fetch_youtube_comments(id, cursor, format, locale, thin_mode, region, sort_by = "top") case cursor when nil, "" diff --git a/src/invidious/comments/reddit_types.cr b/src/invidious/comments/reddit_types.cr new file mode 100644 index 00000000..796a1183 --- /dev/null +++ b/src/invidious/comments/reddit_types.cr @@ -0,0 +1,57 @@ +class RedditThing + include JSON::Serializable + + property kind : String + property data : RedditComment | RedditLink | RedditMore | RedditListing +end + +class RedditComment + include JSON::Serializable + + property author : String + property body_html : String + property replies : RedditThing | String + property score : Int32 + property depth : Int32 + property permalink : String + + @[JSON::Field(converter: RedditComment::TimeConverter)] + property created_utc : Time + + module TimeConverter + def self.from_json(value : JSON::PullParser) : Time + Time.unix(value.read_float.to_i) + end + + def self.to_json(value : Time, json : JSON::Builder) + json.number(value.to_unix) + end + end +end + +struct RedditLink + include JSON::Serializable + + property author : String + property score : Int32 + property subreddit : String + property num_comments : Int32 + property id : String + property permalink : String + property title : String +end + +struct RedditMore + include JSON::Serializable + + property children : Array(String) + property count : Int32 + property depth : Int32 +end + +class RedditListing + include JSON::Serializable + + property children : Array(RedditThing) + property modhash : String +end From 1b25737b013d0589f396fa938ba2747e9a76af93 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 6 May 2023 19:56:30 +0200 Subject: [PATCH 34/41] Comments: Move 'fetch_youtube' function to own file + module --- src/invidious/comments.cr | 203 ------------------------- src/invidious/comments/youtube.cr | 206 ++++++++++++++++++++++++++ src/invidious/routes/api/v1/videos.cr | 2 +- src/invidious/routes/watch.cr | 6 +- 4 files changed, 210 insertions(+), 207 deletions(-) create mode 100644 src/invidious/comments/youtube.cr diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 00e8d399..07579cf3 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -1,206 +1,3 @@ -def fetch_youtube_comments(id, cursor, format, locale, thin_mode, region, sort_by = "top") - case cursor - when nil, "" - ctoken = produce_comment_continuation(id, cursor: "", sort_by: sort_by) - when .starts_with? "ADSJ" - ctoken = produce_comment_continuation(id, cursor: cursor, sort_by: sort_by) - else - ctoken = cursor - end - - client_config = YoutubeAPI::ClientConfig.new(region: region) - response = YoutubeAPI.next(continuation: ctoken, client_config: client_config) - contents = nil - - if on_response_received_endpoints = response["onResponseReceivedEndpoints"]? - header = nil - on_response_received_endpoints.as_a.each do |item| - if item["reloadContinuationItemsCommand"]? - case item["reloadContinuationItemsCommand"]["slot"] - when "RELOAD_CONTINUATION_SLOT_HEADER" - header = item["reloadContinuationItemsCommand"]["continuationItems"][0] - when "RELOAD_CONTINUATION_SLOT_BODY" - # continuationItems is nil when video has no comments - contents = item["reloadContinuationItemsCommand"]["continuationItems"]? - end - elsif item["appendContinuationItemsAction"]? - contents = item["appendContinuationItemsAction"]["continuationItems"] - end - end - elsif response["continuationContents"]? - response = response["continuationContents"] - if response["commentRepliesContinuation"]? - body = response["commentRepliesContinuation"] - else - body = response["itemSectionContinuation"] - end - contents = body["contents"]? - header = body["header"]? - else - raise NotFoundException.new("Comments not found.") - end - - if !contents - if format == "json" - return {"comments" => [] of String}.to_json - else - return {"contentHtml" => "", "commentCount" => 0}.to_json - end - end - - continuation_item_renderer = nil - contents.as_a.reject! do |item| - if item["continuationItemRenderer"]? - continuation_item_renderer = item["continuationItemRenderer"] - true - end - end - - response = JSON.build do |json| - json.object do - if header - count_text = header["commentsHeaderRenderer"]["countText"] - comment_count = (count_text["simpleText"]? || count_text["runs"]?.try &.[0]?.try &.["text"]?) - .try &.as_s.gsub(/\D/, "").to_i? || 0 - json.field "commentCount", comment_count - end - - json.field "videoId", id - - json.field "comments" do - json.array do - contents.as_a.each do |node| - json.object do - if node["commentThreadRenderer"]? - node = node["commentThreadRenderer"] - end - - if node["replies"]? - node_replies = node["replies"]["commentRepliesRenderer"] - end - - if node["comment"]? - node_comment = node["comment"]["commentRenderer"] - else - node_comment = node["commentRenderer"] - end - - content_html = node_comment["contentText"]?.try { |t| parse_content(t, id) } || "" - author = node_comment["authorText"]?.try &.["simpleText"]? || "" - - json.field "verified", (node_comment["authorCommentBadge"]? != nil) - - json.field "author", author - json.field "authorThumbnails" do - json.array do - node_comment["authorThumbnail"]["thumbnails"].as_a.each do |thumbnail| - json.object do - json.field "url", thumbnail["url"] - json.field "width", thumbnail["width"] - json.field "height", thumbnail["height"] - end - end - end - end - - if node_comment["authorEndpoint"]? - json.field "authorId", node_comment["authorEndpoint"]["browseEndpoint"]["browseId"] - json.field "authorUrl", node_comment["authorEndpoint"]["browseEndpoint"]["canonicalBaseUrl"] - else - json.field "authorId", "" - json.field "authorUrl", "" - end - - published_text = node_comment["publishedTimeText"]["runs"][0]["text"].as_s - published = decode_date(published_text.rchop(" (edited)")) - - if published_text.includes?(" (edited)") - json.field "isEdited", true - else - json.field "isEdited", false - end - - json.field "content", html_to_content(content_html) - json.field "contentHtml", content_html - - json.field "isPinned", (node_comment["pinnedCommentBadge"]? != nil) - json.field "isSponsor", (node_comment["sponsorCommentBadge"]? != nil) - if node_comment["sponsorCommentBadge"]? - # Sponsor icon thumbnails always have one object and there's only ever the url property in it - json.field "sponsorIconUrl", node_comment.dig("sponsorCommentBadge", "sponsorCommentBadgeRenderer", "customBadge", "thumbnails", 0, "url").to_s - end - json.field "published", published.to_unix - json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale)) - - comment_action_buttons_renderer = node_comment["actionButtons"]["commentActionButtonsRenderer"] - - json.field "likeCount", comment_action_buttons_renderer["likeButton"]["toggleButtonRenderer"]["accessibilityData"]["accessibilityData"]["label"].as_s.scan(/\d/).map(&.[0]).join.to_i - json.field "commentId", node_comment["commentId"] - json.field "authorIsChannelOwner", node_comment["authorIsChannelOwner"] - - if comment_action_buttons_renderer["creatorHeart"]? - hearth_data = comment_action_buttons_renderer["creatorHeart"]["creatorHeartRenderer"]["creatorThumbnail"] - json.field "creatorHeart" do - json.object do - json.field "creatorThumbnail", hearth_data["thumbnails"][-1]["url"] - json.field "creatorName", hearth_data["accessibility"]["accessibilityData"]["label"] - end - end - end - - if node_replies && !response["commentRepliesContinuation"]? - if node_replies["continuations"]? - continuation = node_replies["continuations"]?.try &.as_a[0]["nextContinuationData"]["continuation"].as_s - elsif node_replies["contents"]? - continuation = node_replies["contents"]?.try &.as_a[0]["continuationItemRenderer"]["continuationEndpoint"]["continuationCommand"]["token"].as_s - end - continuation ||= "" - - json.field "replies" do - json.object do - json.field "replyCount", node_comment["replyCount"]? || 1 - json.field "continuation", continuation - end - end - end - end - end - end - end - - if continuation_item_renderer - if continuation_item_renderer["continuationEndpoint"]? - continuation_endpoint = continuation_item_renderer["continuationEndpoint"] - elsif continuation_item_renderer["button"]? - continuation_endpoint = continuation_item_renderer["button"]["buttonRenderer"]["command"] - end - if continuation_endpoint - json.field "continuation", continuation_endpoint["continuationCommand"]["token"].as_s - end - end - end - end - - if format == "html" - response = JSON.parse(response) - content_html = template_youtube_comments(response, locale, thin_mode) - - response = JSON.build do |json| - json.object do - json.field "contentHtml", content_html - - if response["commentCount"]? - json.field "commentCount", response["commentCount"] - else - json.field "commentCount", 0 - end - end - end - end - - return response -end - def fetch_reddit_comments(id, sort_by = "confidence") client = make_client(REDDIT_URL) headers = HTTP::Headers{"User-Agent" => "web:invidious:v#{CURRENT_VERSION} (by github.com/iv-org/invidious)"} diff --git a/src/invidious/comments/youtube.cr b/src/invidious/comments/youtube.cr new file mode 100644 index 00000000..7e0c8d24 --- /dev/null +++ b/src/invidious/comments/youtube.cr @@ -0,0 +1,206 @@ +module Invidious::Comments + extend self + + def fetch_youtube(id, cursor, format, locale, thin_mode, region, sort_by = "top") + case cursor + when nil, "" + ctoken = produce_comment_continuation(id, cursor: "", sort_by: sort_by) + when .starts_with? "ADSJ" + ctoken = produce_comment_continuation(id, cursor: cursor, sort_by: sort_by) + else + ctoken = cursor + end + + client_config = YoutubeAPI::ClientConfig.new(region: region) + response = YoutubeAPI.next(continuation: ctoken, client_config: client_config) + contents = nil + + if on_response_received_endpoints = response["onResponseReceivedEndpoints"]? + header = nil + on_response_received_endpoints.as_a.each do |item| + if item["reloadContinuationItemsCommand"]? + case item["reloadContinuationItemsCommand"]["slot"] + when "RELOAD_CONTINUATION_SLOT_HEADER" + header = item["reloadContinuationItemsCommand"]["continuationItems"][0] + when "RELOAD_CONTINUATION_SLOT_BODY" + # continuationItems is nil when video has no comments + contents = item["reloadContinuationItemsCommand"]["continuationItems"]? + end + elsif item["appendContinuationItemsAction"]? + contents = item["appendContinuationItemsAction"]["continuationItems"] + end + end + elsif response["continuationContents"]? + response = response["continuationContents"] + if response["commentRepliesContinuation"]? + body = response["commentRepliesContinuation"] + else + body = response["itemSectionContinuation"] + end + contents = body["contents"]? + header = body["header"]? + else + raise NotFoundException.new("Comments not found.") + end + + if !contents + if format == "json" + return {"comments" => [] of String}.to_json + else + return {"contentHtml" => "", "commentCount" => 0}.to_json + end + end + + continuation_item_renderer = nil + contents.as_a.reject! do |item| + if item["continuationItemRenderer"]? + continuation_item_renderer = item["continuationItemRenderer"] + true + end + end + + response = JSON.build do |json| + json.object do + if header + count_text = header["commentsHeaderRenderer"]["countText"] + comment_count = (count_text["simpleText"]? || count_text["runs"]?.try &.[0]?.try &.["text"]?) + .try &.as_s.gsub(/\D/, "").to_i? || 0 + json.field "commentCount", comment_count + end + + json.field "videoId", id + + json.field "comments" do + json.array do + contents.as_a.each do |node| + json.object do + if node["commentThreadRenderer"]? + node = node["commentThreadRenderer"] + end + + if node["replies"]? + node_replies = node["replies"]["commentRepliesRenderer"] + end + + if node["comment"]? + node_comment = node["comment"]["commentRenderer"] + else + node_comment = node["commentRenderer"] + end + + content_html = node_comment["contentText"]?.try { |t| parse_content(t, id) } || "" + author = node_comment["authorText"]?.try &.["simpleText"]? || "" + + json.field "verified", (node_comment["authorCommentBadge"]? != nil) + + json.field "author", author + json.field "authorThumbnails" do + json.array do + node_comment["authorThumbnail"]["thumbnails"].as_a.each do |thumbnail| + json.object do + json.field "url", thumbnail["url"] + json.field "width", thumbnail["width"] + json.field "height", thumbnail["height"] + end + end + end + end + + if node_comment["authorEndpoint"]? + json.field "authorId", node_comment["authorEndpoint"]["browseEndpoint"]["browseId"] + json.field "authorUrl", node_comment["authorEndpoint"]["browseEndpoint"]["canonicalBaseUrl"] + else + json.field "authorId", "" + json.field "authorUrl", "" + end + + published_text = node_comment["publishedTimeText"]["runs"][0]["text"].as_s + published = decode_date(published_text.rchop(" (edited)")) + + if published_text.includes?(" (edited)") + json.field "isEdited", true + else + json.field "isEdited", false + end + + json.field "content", html_to_content(content_html) + json.field "contentHtml", content_html + + json.field "isPinned", (node_comment["pinnedCommentBadge"]? != nil) + json.field "isSponsor", (node_comment["sponsorCommentBadge"]? != nil) + if node_comment["sponsorCommentBadge"]? + # Sponsor icon thumbnails always have one object and there's only ever the url property in it + json.field "sponsorIconUrl", node_comment.dig("sponsorCommentBadge", "sponsorCommentBadgeRenderer", "customBadge", "thumbnails", 0, "url").to_s + end + json.field "published", published.to_unix + json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale)) + + comment_action_buttons_renderer = node_comment["actionButtons"]["commentActionButtonsRenderer"] + + json.field "likeCount", comment_action_buttons_renderer["likeButton"]["toggleButtonRenderer"]["accessibilityData"]["accessibilityData"]["label"].as_s.scan(/\d/).map(&.[0]).join.to_i + json.field "commentId", node_comment["commentId"] + json.field "authorIsChannelOwner", node_comment["authorIsChannelOwner"] + + if comment_action_buttons_renderer["creatorHeart"]? + hearth_data = comment_action_buttons_renderer["creatorHeart"]["creatorHeartRenderer"]["creatorThumbnail"] + json.field "creatorHeart" do + json.object do + json.field "creatorThumbnail", hearth_data["thumbnails"][-1]["url"] + json.field "creatorName", hearth_data["accessibility"]["accessibilityData"]["label"] + end + end + end + + if node_replies && !response["commentRepliesContinuation"]? + if node_replies["continuations"]? + continuation = node_replies["continuations"]?.try &.as_a[0]["nextContinuationData"]["continuation"].as_s + elsif node_replies["contents"]? + continuation = node_replies["contents"]?.try &.as_a[0]["continuationItemRenderer"]["continuationEndpoint"]["continuationCommand"]["token"].as_s + end + continuation ||= "" + + json.field "replies" do + json.object do + json.field "replyCount", node_comment["replyCount"]? || 1 + json.field "continuation", continuation + end + end + end + end + end + end + end + + if continuation_item_renderer + if continuation_item_renderer["continuationEndpoint"]? + continuation_endpoint = continuation_item_renderer["continuationEndpoint"] + elsif continuation_item_renderer["button"]? + continuation_endpoint = continuation_item_renderer["button"]["buttonRenderer"]["command"] + end + if continuation_endpoint + json.field "continuation", continuation_endpoint["continuationCommand"]["token"].as_s + end + end + end + end + + if format == "html" + response = JSON.parse(response) + content_html = template_youtube_comments(response, locale, thin_mode) + + response = JSON.build do |json| + json.object do + json.field "contentHtml", content_html + + if response["commentCount"]? + json.field "commentCount", response["commentCount"] + else + json.field "commentCount", 0 + end + end + end + end + + return response + end +end diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index f312211e..ce3e96d2 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -333,7 +333,7 @@ module Invidious::Routes::API::V1::Videos sort_by ||= "top" begin - comments = fetch_youtube_comments(id, continuation, format, locale, thin_mode, region, sort_by: sort_by) + comments = Comments.fetch_youtube(id, continuation, format, locale, thin_mode, region, sort_by: sort_by) rescue ex : NotFoundException return error_json(404, ex) rescue ex diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 813cb0f4..861b25c2 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -95,7 +95,7 @@ module Invidious::Routes::Watch if source == "youtube" begin - comment_html = JSON.parse(fetch_youtube_comments(id, nil, "html", locale, preferences.thin_mode, region))["contentHtml"] + comment_html = JSON.parse(Comments.fetch_youtube(id, nil, "html", locale, preferences.thin_mode, region))["contentHtml"] rescue ex if preferences.comments[1] == "reddit" comments, reddit_thread = fetch_reddit_comments(id) @@ -114,12 +114,12 @@ module Invidious::Routes::Watch comment_html = replace_links(comment_html) rescue ex if preferences.comments[1] == "youtube" - comment_html = JSON.parse(fetch_youtube_comments(id, nil, "html", locale, preferences.thin_mode, region))["contentHtml"] + comment_html = JSON.parse(Comments.fetch_youtube(id, nil, "html", locale, preferences.thin_mode, region))["contentHtml"] end end end else - comment_html = JSON.parse(fetch_youtube_comments(id, nil, "html", locale, preferences.thin_mode, region))["contentHtml"] + comment_html = JSON.parse(Comments.fetch_youtube(id, nil, "html", locale, preferences.thin_mode, region))["contentHtml"] end comment_html ||= "" From 634e913da9381f5212a1017e2f4a37e7d7075204 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 6 May 2023 20:02:42 +0200 Subject: [PATCH 35/41] Comments: Move 'fetch_reddit' function to own file + module --- src/invidious/comments.cr | 38 ------------------------- src/invidious/comments/reddit.cr | 41 +++++++++++++++++++++++++++ src/invidious/routes/api/v1/videos.cr | 2 +- src/invidious/routes/watch.cr | 4 +-- 4 files changed, 44 insertions(+), 41 deletions(-) create mode 100644 src/invidious/comments/reddit.cr diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 07579cf3..07b92786 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -1,41 +1,3 @@ -def fetch_reddit_comments(id, sort_by = "confidence") - client = make_client(REDDIT_URL) - headers = HTTP::Headers{"User-Agent" => "web:invidious:v#{CURRENT_VERSION} (by github.com/iv-org/invidious)"} - - # TODO: Use something like #479 for a static list of instances to use here - query = URI::Params.encode({q: "(url:3D#{id} OR url:#{id}) AND (site:invidio.us OR site:youtube.com OR site:youtu.be)"}) - search_results = client.get("/search.json?#{query}", headers) - - if search_results.status_code == 200 - search_results = RedditThing.from_json(search_results.body) - - # For videos that have more than one thread, choose the one with the highest score - threads = search_results.data.as(RedditListing).children - thread = threads.max_by?(&.data.as(RedditLink).score).try(&.data.as(RedditLink)) - result = thread.try do |t| - body = client.get("/r/#{t.subreddit}/comments/#{t.id}.json?limit=100&sort=#{sort_by}", headers).body - Array(RedditThing).from_json(body) - end - result ||= [] of RedditThing - elsif search_results.status_code == 302 - # Previously, if there was only one result then the API would redirect to that result. - # Now, it appears it will still return a listing so this section is likely unnecessary. - - result = client.get(search_results.headers["Location"], headers).body - result = Array(RedditThing).from_json(result) - - thread = result[0].data.as(RedditListing).children[0].data.as(RedditLink) - else - raise NotFoundException.new("Comments not found.") - end - - client.close - - comments = result[1]?.try(&.data.as(RedditListing).children) - comments ||= [] of RedditThing - return comments, thread -end - def template_youtube_comments(comments, locale, thin_mode, is_replies = false) String.build do |html| root = comments["comments"].as_a diff --git a/src/invidious/comments/reddit.cr b/src/invidious/comments/reddit.cr new file mode 100644 index 00000000..ba9c19f1 --- /dev/null +++ b/src/invidious/comments/reddit.cr @@ -0,0 +1,41 @@ +module Invidious::Comments + extend self + + def fetch_reddit(id, sort_by = "confidence") + client = make_client(REDDIT_URL) + headers = HTTP::Headers{"User-Agent" => "web:invidious:v#{CURRENT_VERSION} (by github.com/iv-org/invidious)"} + + # TODO: Use something like #479 for a static list of instances to use here + query = URI::Params.encode({q: "(url:3D#{id} OR url:#{id}) AND (site:invidio.us OR site:youtube.com OR site:youtu.be)"}) + search_results = client.get("/search.json?#{query}", headers) + + if search_results.status_code == 200 + search_results = RedditThing.from_json(search_results.body) + + # For videos that have more than one thread, choose the one with the highest score + threads = search_results.data.as(RedditListing).children + thread = threads.max_by?(&.data.as(RedditLink).score).try(&.data.as(RedditLink)) + result = thread.try do |t| + body = client.get("/r/#{t.subreddit}/comments/#{t.id}.json?limit=100&sort=#{sort_by}", headers).body + Array(RedditThing).from_json(body) + end + result ||= [] of RedditThing + elsif search_results.status_code == 302 + # Previously, if there was only one result then the API would redirect to that result. + # Now, it appears it will still return a listing so this section is likely unnecessary. + + result = client.get(search_results.headers["Location"], headers).body + result = Array(RedditThing).from_json(result) + + thread = result[0].data.as(RedditListing).children[0].data.as(RedditLink) + else + raise NotFoundException.new("Comments not found.") + end + + client.close + + comments = result[1]?.try(&.data.as(RedditListing).children) + comments ||= [] of RedditThing + return comments, thread + end +end diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index ce3e96d2..cb1008ac 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -345,7 +345,7 @@ module Invidious::Routes::API::V1::Videos sort_by ||= "confidence" begin - comments, reddit_thread = fetch_reddit_comments(id, sort_by: sort_by) + comments, reddit_thread = Comments.fetch_reddit(id, sort_by: sort_by) rescue ex comments = nil reddit_thread = nil diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 861b25c2..b08e6fbe 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -98,7 +98,7 @@ module Invidious::Routes::Watch comment_html = JSON.parse(Comments.fetch_youtube(id, nil, "html", locale, preferences.thin_mode, region))["contentHtml"] rescue ex if preferences.comments[1] == "reddit" - comments, reddit_thread = fetch_reddit_comments(id) + comments, reddit_thread = Comments.fetch_reddit(id) comment_html = template_reddit_comments(comments, locale) comment_html = fill_links(comment_html, "https", "www.reddit.com") @@ -107,7 +107,7 @@ module Invidious::Routes::Watch end elsif source == "reddit" begin - comments, reddit_thread = fetch_reddit_comments(id) + comments, reddit_thread = Comments.fetch_reddit(id) comment_html = template_reddit_comments(comments, locale) comment_html = fill_links(comment_html, "https", "www.reddit.com") From e10f6b6626bfe462861980184b09b7350499c889 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 6 May 2023 20:07:13 +0200 Subject: [PATCH 36/41] Comments: Move 'template_youtube' function to own file + module --- src/invidious/channels/community.cr | 2 +- src/invidious/comments.cr | 157 -------------------- src/invidious/comments/youtube.cr | 2 +- src/invidious/frontend/comments_youtube.cr | 160 +++++++++++++++++++++ src/invidious/views/community.ecr | 2 +- 5 files changed, 163 insertions(+), 160 deletions(-) create mode 100644 src/invidious/frontend/comments_youtube.cr diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr index 2c7b9fec..aac4bc8a 100644 --- a/src/invidious/channels/community.cr +++ b/src/invidious/channels/community.cr @@ -250,7 +250,7 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) if format == "html" response = JSON.parse(response) - content_html = template_youtube_comments(response, locale, thin_mode) + content_html = IV::Frontend::Comments.template_youtube(response, locale, thin_mode) response = JSON.build do |json| json.object do diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 07b92786..8943b1da 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -1,160 +1,3 @@ -def template_youtube_comments(comments, locale, thin_mode, is_replies = false) - String.build do |html| - root = comments["comments"].as_a - root.each do |child| - if child["replies"]? - replies_count_text = translate_count(locale, - "comments_view_x_replies", - child["replies"]["replyCount"].as_i64 || 0, - NumberFormatting::Separator - ) - - replies_html = <<-END_HTML -
-
- -
- END_HTML - end - - if !thin_mode - author_thumbnail = "/ggpht#{URI.parse(child["authorThumbnails"][-1]["url"].as_s).request_target}" - else - author_thumbnail = "" - end - - author_name = HTML.escape(child["author"].as_s) - sponsor_icon = "" - if child["verified"]?.try &.as_bool && child["authorIsChannelOwner"]?.try &.as_bool - author_name += " " - elsif child["verified"]?.try &.as_bool - author_name += " " - end - - if child["isSponsor"]?.try &.as_bool - sponsor_icon = String.build do |str| - str << %() - end - end - html << <<-END_HTML -
-
- -
-
-

- - #{author_name} - - #{sponsor_icon} -

#{child["contentHtml"]}

- END_HTML - - if child["attachment"]? - attachment = child["attachment"] - - case attachment["type"] - when "image" - attachment = attachment["imageThumbnails"][1] - - html << <<-END_HTML -
-
- -
-
- END_HTML - when "video" - if attachment["error"]? - html << <<-END_HTML -
-

#{attachment["error"]}

-
- END_HTML - else - html << <<-END_HTML -
- -
- END_HTML - end - else nil # Ignore - end - end - - html << <<-END_HTML -

- #{translate(locale, "`x` ago", recode_date(Time.unix(child["published"].as_i64), locale))} #{child["isEdited"] == true ? translate(locale, "(edited)") : ""} - | - END_HTML - - if comments["videoId"]? - html << <<-END_HTML - [YT] - | - END_HTML - elsif comments["authorId"]? - html << <<-END_HTML - [YT] - | - END_HTML - end - - html << <<-END_HTML - #{number_with_separator(child["likeCount"])} - END_HTML - - if child["creatorHeart"]? - if !thin_mode - creator_thumbnail = "/ggpht#{URI.parse(child["creatorHeart"]["creatorThumbnail"].as_s).request_target}" - else - creator_thumbnail = "" - end - - html << <<-END_HTML -   - - - - - - - - - END_HTML - end - - html << <<-END_HTML -

- #{replies_html} -
-
- END_HTML - end - - if comments["continuation"]? - html << <<-END_HTML - - END_HTML - end - end -end - def template_reddit_comments(root, locale) String.build do |html| root.each do |child| diff --git a/src/invidious/comments/youtube.cr b/src/invidious/comments/youtube.cr index 7e0c8d24..c262876e 100644 --- a/src/invidious/comments/youtube.cr +++ b/src/invidious/comments/youtube.cr @@ -186,7 +186,7 @@ module Invidious::Comments if format == "html" response = JSON.parse(response) - content_html = template_youtube_comments(response, locale, thin_mode) + content_html = Frontend::Comments.template_youtube(response, locale, thin_mode) response = JSON.build do |json| json.object do diff --git a/src/invidious/frontend/comments_youtube.cr b/src/invidious/frontend/comments_youtube.cr new file mode 100644 index 00000000..41f43f04 --- /dev/null +++ b/src/invidious/frontend/comments_youtube.cr @@ -0,0 +1,160 @@ +module Invidious::Frontend::Comments + extend self + + def template_youtube(comments, locale, thin_mode, is_replies = false) + String.build do |html| + root = comments["comments"].as_a + root.each do |child| + if child["replies"]? + replies_count_text = translate_count(locale, + "comments_view_x_replies", + child["replies"]["replyCount"].as_i64 || 0, + NumberFormatting::Separator + ) + + replies_html = <<-END_HTML +
+
+ +
+ END_HTML + end + + if !thin_mode + author_thumbnail = "/ggpht#{URI.parse(child["authorThumbnails"][-1]["url"].as_s).request_target}" + else + author_thumbnail = "" + end + + author_name = HTML.escape(child["author"].as_s) + sponsor_icon = "" + if child["verified"]?.try &.as_bool && child["authorIsChannelOwner"]?.try &.as_bool + author_name += " " + elsif child["verified"]?.try &.as_bool + author_name += " " + end + + if child["isSponsor"]?.try &.as_bool + sponsor_icon = String.build do |str| + str << %() + end + end + html << <<-END_HTML +
+
+ +
+
+

+ + #{author_name} + + #{sponsor_icon} +

#{child["contentHtml"]}

+ END_HTML + + if child["attachment"]? + attachment = child["attachment"] + + case attachment["type"] + when "image" + attachment = attachment["imageThumbnails"][1] + + html << <<-END_HTML +
+
+ +
+
+ END_HTML + when "video" + if attachment["error"]? + html << <<-END_HTML +
+

#{attachment["error"]}

+
+ END_HTML + else + html << <<-END_HTML +
+ +
+ END_HTML + end + else nil # Ignore + end + end + + html << <<-END_HTML +

+ #{translate(locale, "`x` ago", recode_date(Time.unix(child["published"].as_i64), locale))} #{child["isEdited"] == true ? translate(locale, "(edited)") : ""} + | + END_HTML + + if comments["videoId"]? + html << <<-END_HTML + [YT] + | + END_HTML + elsif comments["authorId"]? + html << <<-END_HTML + [YT] + | + END_HTML + end + + html << <<-END_HTML + #{number_with_separator(child["likeCount"])} + END_HTML + + if child["creatorHeart"]? + if !thin_mode + creator_thumbnail = "/ggpht#{URI.parse(child["creatorHeart"]["creatorThumbnail"].as_s).request_target}" + else + creator_thumbnail = "" + end + + html << <<-END_HTML +   + + + + + + + + + END_HTML + end + + html << <<-END_HTML +

+ #{replies_html} +
+
+ END_HTML + end + + if comments["continuation"]? + html << <<-END_HTML + + END_HTML + end + end + end +end diff --git a/src/invidious/views/community.ecr b/src/invidious/views/community.ecr index 9e11d562..24efc34e 100644 --- a/src/invidious/views/community.ecr +++ b/src/invidious/views/community.ecr @@ -27,7 +27,7 @@
<% else %>
- <%= template_youtube_comments(items.not_nil!, locale, thin_mode) %> + <%= IV::Frontend::Comments.template_youtube(items.not_nil!, locale, thin_mode) %>
<% end %> From de78848039c2e5e8dea25b6013f3e24797a0b1ce Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 6 May 2023 20:12:02 +0200 Subject: [PATCH 37/41] Comments: Move 'template_reddit' function to own file + module --- src/invidious/comments.cr | 47 --------------------- src/invidious/frontend/comments_reddit.cr | 50 +++++++++++++++++++++++ src/invidious/routes/api/v1/videos.cr | 2 +- src/invidious/routes/watch.cr | 4 +- 4 files changed, 53 insertions(+), 50 deletions(-) create mode 100644 src/invidious/frontend/comments_reddit.cr diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 8943b1da..6a3aa4c2 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -1,50 +1,3 @@ -def template_reddit_comments(root, locale) - String.build do |html| - root.each do |child| - if child.data.is_a?(RedditComment) - child = child.data.as(RedditComment) - body_html = HTML.unescape(child.body_html) - - replies_html = "" - if child.replies.is_a?(RedditThing) - replies = child.replies.as(RedditThing) - replies_html = template_reddit_comments(replies.data.as(RedditListing).children, locale) - end - - if child.depth > 0 - html << <<-END_HTML -
-
-
-
- END_HTML - else - html << <<-END_HTML -
-
- END_HTML - end - - html << <<-END_HTML -

- [ − ] - #{child.author} - #{translate_count(locale, "comments_points_count", child.score, NumberFormatting::Separator)} - #{translate(locale, "`x` ago", recode_date(child.created_utc, locale))} - #{translate(locale, "permalink")} -

-
- #{body_html} - #{replies_html} -
-
-
- END_HTML - end - end - end -end - def replace_links(html) # Check if the document is empty # Prevents edge-case bug with Reddit comments, see issue #3115 diff --git a/src/invidious/frontend/comments_reddit.cr b/src/invidious/frontend/comments_reddit.cr new file mode 100644 index 00000000..b5647bae --- /dev/null +++ b/src/invidious/frontend/comments_reddit.cr @@ -0,0 +1,50 @@ +module Invidious::Frontend::Comments + extend self + + def template_reddit(root, locale) + String.build do |html| + root.each do |child| + if child.data.is_a?(RedditComment) + child = child.data.as(RedditComment) + body_html = HTML.unescape(child.body_html) + + replies_html = "" + if child.replies.is_a?(RedditThing) + replies = child.replies.as(RedditThing) + replies_html = self.template_reddit(replies.data.as(RedditListing).children, locale) + end + + if child.depth > 0 + html << <<-END_HTML +
+
+
+
+ END_HTML + else + html << <<-END_HTML +
+
+ END_HTML + end + + html << <<-END_HTML +

+ [ − ] + #{child.author} + #{translate_count(locale, "comments_points_count", child.score, NumberFormatting::Separator)} + #{translate(locale, "`x` ago", recode_date(child.created_utc, locale))} + #{translate(locale, "permalink")} +

+
+ #{body_html} + #{replies_html} +
+
+
+ END_HTML + end + end + end + end +end diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index cb1008ac..6feaaef4 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -361,7 +361,7 @@ module Invidious::Routes::API::V1::Videos return reddit_thread.to_json else - content_html = template_reddit_comments(comments, locale) + content_html = Frontend::Comments.template_reddit(comments, locale) content_html = fill_links(content_html, "https", "www.reddit.com") content_html = replace_links(content_html) response = { diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index b08e6fbe..6b441a48 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -99,7 +99,7 @@ module Invidious::Routes::Watch rescue ex if preferences.comments[1] == "reddit" comments, reddit_thread = Comments.fetch_reddit(id) - comment_html = template_reddit_comments(comments, locale) + comment_html = Frontend::Comments.template_reddit(comments, locale) comment_html = fill_links(comment_html, "https", "www.reddit.com") comment_html = replace_links(comment_html) @@ -108,7 +108,7 @@ module Invidious::Routes::Watch elsif source == "reddit" begin comments, reddit_thread = Comments.fetch_reddit(id) - comment_html = template_reddit_comments(comments, locale) + comment_html = Frontend::Comments.template_reddit(comments, locale) comment_html = fill_links(comment_html, "https", "www.reddit.com") comment_html = replace_links(comment_html) From df8526545383f4def3605fb61551edbd851c18c7 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 6 May 2023 20:20:27 +0200 Subject: [PATCH 38/41] Comments: Move link utility functions to own file + module --- src/invidious/comments.cr | 73 ------------------------- src/invidious/comments/links_util.cr | 76 +++++++++++++++++++++++++++ src/invidious/routes/api/v1/videos.cr | 4 +- src/invidious/routes/watch.cr | 8 +-- 4 files changed, 82 insertions(+), 79 deletions(-) create mode 100644 src/invidious/comments/links_util.cr diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 6a3aa4c2..3c7e2bb4 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -1,76 +1,3 @@ -def replace_links(html) - # Check if the document is empty - # Prevents edge-case bug with Reddit comments, see issue #3115 - if html.nil? || html.empty? - return html - end - - html = XML.parse_html(html) - - html.xpath_nodes(%q(//a)).each do |anchor| - url = URI.parse(anchor["href"]) - - if url.host.nil? || url.host.not_nil!.ends_with?("youtube.com") || url.host.not_nil!.ends_with?("youtu.be") - if url.host.try &.ends_with? "youtu.be" - url = "/watch?v=#{url.path.lstrip('/')}#{url.query_params}" - else - if url.path == "/redirect" - params = HTTP::Params.parse(url.query.not_nil!) - anchor["href"] = params["q"]? - else - anchor["href"] = url.request_target - end - end - elsif url.to_s == "#" - begin - length_seconds = decode_length_seconds(anchor.content) - rescue ex - length_seconds = decode_time(anchor.content) - end - - if length_seconds > 0 - anchor["href"] = "javascript:void(0)" - anchor["onclick"] = "player.currentTime(#{length_seconds})" - else - anchor["href"] = url.request_target - end - end - end - - html = html.xpath_node(%q(//body)).not_nil! - if node = html.xpath_node(%q(./p)) - html = node - end - - return html.to_xml(options: XML::SaveOptions::NO_DECL) -end - -def fill_links(html, scheme, host) - # Check if the document is empty - # Prevents edge-case bug with Reddit comments, see issue #3115 - if html.nil? || html.empty? - return html - end - - html = XML.parse_html(html) - - html.xpath_nodes("//a").each do |match| - url = URI.parse(match["href"]) - # Reddit links don't have host - if !url.host && !match["href"].starts_with?("javascript") && !url.to_s.ends_with? "#" - url.scheme = scheme - url.host = host - match["href"] = url - end - end - - if host == "www.youtube.com" - html = html.xpath_node(%q(//body/p)).not_nil! - end - - return html.to_xml(options: XML::SaveOptions::NO_DECL) -end - def text_to_parsed_content(text : String) : JSON::Any nodes = [] of JSON::Any # For each line convert line to array of nodes diff --git a/src/invidious/comments/links_util.cr b/src/invidious/comments/links_util.cr new file mode 100644 index 00000000..f89b86d3 --- /dev/null +++ b/src/invidious/comments/links_util.cr @@ -0,0 +1,76 @@ +module Invidious::Comments + extend self + + def replace_links(html) + # Check if the document is empty + # Prevents edge-case bug with Reddit comments, see issue #3115 + if html.nil? || html.empty? + return html + end + + html = XML.parse_html(html) + + html.xpath_nodes(%q(//a)).each do |anchor| + url = URI.parse(anchor["href"]) + + if url.host.nil? || url.host.not_nil!.ends_with?("youtube.com") || url.host.not_nil!.ends_with?("youtu.be") + if url.host.try &.ends_with? "youtu.be" + url = "/watch?v=#{url.path.lstrip('/')}#{url.query_params}" + else + if url.path == "/redirect" + params = HTTP::Params.parse(url.query.not_nil!) + anchor["href"] = params["q"]? + else + anchor["href"] = url.request_target + end + end + elsif url.to_s == "#" + begin + length_seconds = decode_length_seconds(anchor.content) + rescue ex + length_seconds = decode_time(anchor.content) + end + + if length_seconds > 0 + anchor["href"] = "javascript:void(0)" + anchor["onclick"] = "player.currentTime(#{length_seconds})" + else + anchor["href"] = url.request_target + end + end + end + + html = html.xpath_node(%q(//body)).not_nil! + if node = html.xpath_node(%q(./p)) + html = node + end + + return html.to_xml(options: XML::SaveOptions::NO_DECL) + end + + def fill_links(html, scheme, host) + # Check if the document is empty + # Prevents edge-case bug with Reddit comments, see issue #3115 + if html.nil? || html.empty? + return html + end + + html = XML.parse_html(html) + + html.xpath_nodes("//a").each do |match| + url = URI.parse(match["href"]) + # Reddit links don't have host + if !url.host && !match["href"].starts_with?("javascript") && !url.to_s.ends_with? "#" + url.scheme = scheme + url.host = host + match["href"] = url + end + end + + if host == "www.youtube.com" + html = html.xpath_node(%q(//body/p)).not_nil! + end + + return html.to_xml(options: XML::SaveOptions::NO_DECL) + end +end diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index 6feaaef4..af4fc806 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -362,8 +362,8 @@ module Invidious::Routes::API::V1::Videos return reddit_thread.to_json else content_html = Frontend::Comments.template_reddit(comments, locale) - content_html = fill_links(content_html, "https", "www.reddit.com") - content_html = replace_links(content_html) + content_html = Comments.fill_links(content_html, "https", "www.reddit.com") + content_html = Comments.replace_links(content_html) response = { "title" => reddit_thread.title, "permalink" => reddit_thread.permalink, diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 6b441a48..e5cf3716 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -101,8 +101,8 @@ module Invidious::Routes::Watch comments, reddit_thread = Comments.fetch_reddit(id) comment_html = Frontend::Comments.template_reddit(comments, locale) - comment_html = fill_links(comment_html, "https", "www.reddit.com") - comment_html = replace_links(comment_html) + comment_html = Comments.fill_links(comment_html, "https", "www.reddit.com") + comment_html = Comments.replace_links(comment_html) end end elsif source == "reddit" @@ -110,8 +110,8 @@ module Invidious::Routes::Watch comments, reddit_thread = Comments.fetch_reddit(id) comment_html = Frontend::Comments.template_reddit(comments, locale) - comment_html = fill_links(comment_html, "https", "www.reddit.com") - comment_html = replace_links(comment_html) + comment_html = Comments.fill_links(comment_html, "https", "www.reddit.com") + comment_html = Comments.replace_links(comment_html) rescue ex if preferences.comments[1] == "youtube" comment_html = JSON.parse(Comments.fetch_youtube(id, nil, "html", locale, preferences.thin_mode, region))["contentHtml"] From 4379a3d873540460859ec30845dfba66a33d0aea Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 6 May 2023 20:23:47 +0200 Subject: [PATCH 39/41] Comments: Move ctoken functions to youtube.cr --- spec/invidious/helpers_spec.cr | 12 -------- src/invidious/comments.cr | 44 ---------------------------- src/invidious/comments/youtube.cr | 48 +++++++++++++++++++++++++++++-- 3 files changed, 46 insertions(+), 58 deletions(-) diff --git a/spec/invidious/helpers_spec.cr b/spec/invidious/helpers_spec.cr index f81cd29a..142e1653 100644 --- a/spec/invidious/helpers_spec.cr +++ b/spec/invidious/helpers_spec.cr @@ -23,18 +23,6 @@ Spectator.describe "Helper" do end end - describe "#produce_comment_continuation" do - it "correctly produces a continuation token for comments" do - expect(produce_comment_continuation("_cE8xSu6swE", "ADSJ_i2qvJeFtL0htmS5_K5Ctj3eGFVBMWL9Wd42o3kmUL6_mAzdLp85-liQZL0mYr_16BhaggUqX652Sv9JqV6VXinShSP-ZT6rL4NolPBaPXVtJsO5_rA_qE3GubAuLFw9uzIIXU2-HnpXbdgPLWTFavfX206hqWmmpHwUOrmxQV_OX6tYkM3ux3rPAKCDrT8eWL7MU3bLiNcnbgkW8o0h8KYLL_8BPa8LcHbTv8pAoNkjerlX1x7K4pqxaXPoyz89qNlnh6rRx6AXgAzzoHH1dmcyQ8CIBeOHg-m4i8ZxdX4dP88XWrIFg-jJGhpGP8JUMDgZgavxVx225hUEYZMyrLGler5em4FgbG62YWC51moLDLeYEA")).to eq("EkMSC19jRTh4U3U2c3dFyAEA4AEBogINKP___________wFAAMICHQgEGhdodHRwczovL3d3dy55b3V0dWJlLmNvbSIAGAYyjAMK9gJBRFNKX2kycXZKZUZ0TDBodG1TNV9LNUN0ajNlR0ZWQk1XTDlXZDQybzNrbVVMNl9tQXpkTHA4NS1saVFaTDBtWXJfMTZCaGFnZ1VxWDY1MlN2OUpxVjZWWGluU2hTUC1aVDZyTDROb2xQQmFQWFZ0SnNPNV9yQV9xRTNHdWJBdUxGdzl1eklJWFUyLUhucFhiZGdQTFdURmF2ZlgyMDZocVdtbXBId1VPcm14UVZfT1g2dFlrTTN1eDNyUEFLQ0RyVDhlV0w3TVUzYkxpTmNuYmdrVzhvMGg4S1lMTF84QlBhOExjSGJUdjhwQW9Oa2plcmxYMXg3SzRwcXhhWFBveXo4OXFObG5oNnJSeDZBWGdBenpvSEgxZG1jeVE4Q0lCZU9IZy1tNGk4WnhkWDRkUDg4WFdySUZnLWpKR2hwR1A4SlVNRGdaZ2F2eFZ4MjI1aFVFWVpNeXJMR2xlcjVlbTRGZ2JHNjJZV0M1MW1vTERMZVlFQSIPIgtfY0U4eFN1NnN3RTAAKBQ%3D") - - expect(produce_comment_continuation("_cE8xSu6swE", "ADSJ_i1yz21HI4xrtsYXVC-2_kfZ6kx1yjYQumXAAxqH3CAd7ZxKxfLdZS1__fqhCtOASRbbpSBGH_tH1J96Dxux-Qfjk-lUbupMqv08Q3aHzGu7p70VoUMHhI2-GoJpnbpmcOxkGzeIuenRS_ym2Y8fkDowhqLPFgsS0n4djnZ2UmC17F3Ch3N1S1UYf1ZVOc991qOC1iW9kJDzyvRQTWCPsJUPneSaAKW-Rr97pdesOkR4i8cNvHZRnQKe2HEfsvlJOb2C3lF1dJBfJeNfnQYeh5hv6_fZN7bt3-JL1Xk3Qc9NXNxmmbDpwAC_yFR8dthFfUJdyIO9Nu1D79MLYeR-H5HxqUJokkJiGIz4lTE_CXXbhAI")).to eq("EkMSC19jRTh4U3U2c3dFyAEA4AEBogINKP___________wFAAMICHQgEGhdodHRwczovL3d3dy55b3V0dWJlLmNvbSIAGAYyiQMK8wJBRFNKX2kxeXoyMUhJNHhydHNZWFZDLTJfa2ZaNmt4MXlqWVF1bVhBQXhxSDNDQWQ3WnhLeGZMZFpTMV9fZnFoQ3RPQVNSYmJwU0JHSF90SDFKOTZEeHV4LVFmamstbFVidXBNcXYwOFEzYUh6R3U3cDcwVm9VTUhoSTItR29KcG5icG1jT3hrR3plSXVlblJTX3ltMlk4ZmtEb3docUxQRmdzUzBuNGRqbloyVW1DMTdGM0NoM04xUzFVWWYxWlZPYzk5MXFPQzFpVzlrSkR6eXZSUVRXQ1BzSlVQbmVTYUFLVy1Scjk3cGRlc09rUjRpOGNOdkhaUm5RS2UySEVmc3ZsSk9iMkMzbEYxZEpCZkplTmZuUVllaDVodjZfZlpON2J0My1KTDFYazNRYzlOWE54bW1iRHB3QUNfeUZSOGR0aEZmVUpkeUlPOU51MUQ3OU1MWWVSLUg1SHhxVUpva2tKaUdJejRsVEVfQ1hYYmhBSSIPIgtfY0U4eFN1NnN3RTAAKBQ%3D") - - expect(produce_comment_continuation("29-q7YnyUmY", "")).to eq("EkMSCzI5LXE3WW55VW1ZyAEA4AEBogINKP___________wFAAMICHQgEGhdodHRwczovL3d3dy55b3V0dWJlLmNvbSIAGAYyFQoAIg8iCzI5LXE3WW55VW1ZMAAoFA%3D%3D") - - expect(produce_comment_continuation("CvFH_6DNRCY", "")).to eq("EkMSC0N2RkhfNkROUkNZyAEA4AEBogINKP___________wFAAMICHQgEGhdodHRwczovL3d3dy55b3V0dWJlLmNvbSIAGAYyFQoAIg8iC0N2RkhfNkROUkNZMAAoFA%3D%3D") - end - end - describe "#produce_channel_community_continuation" do it "correctly produces a continuation token for a channel community" do expect(produce_channel_community_continuation("UCCj956IF62FbT7Gouszaj9w", "Egljb21tdW5pdHm4")).to eq("4qmFsgIsEhhVQ0NqOTU2SUY2MkZiVDdHb3VzemFqOXcaEEVnbGpiMjF0ZFc1cGRIbTQ%3D") diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 3c7e2bb4..c8cdc2df 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -87,47 +87,3 @@ def content_to_comment_html(content, video_id : String? = "") return html_array.join("").delete('\ufeff') end - -def produce_comment_continuation(video_id, cursor = "", sort_by = "top") - object = { - "2:embedded" => { - "2:string" => video_id, - "25:varint" => 0_i64, - "28:varint" => 1_i64, - "36:embedded" => { - "5:varint" => -1_i64, - "8:varint" => 0_i64, - }, - "40:embedded" => { - "1:varint" => 4_i64, - "3:string" => "https://www.youtube.com", - "4:string" => "", - }, - }, - "3:varint" => 6_i64, - "6:embedded" => { - "1:string" => cursor, - "4:embedded" => { - "4:string" => video_id, - "6:varint" => 0_i64, - }, - "5:varint" => 20_i64, - }, - } - - case sort_by - when "top" - object["6:embedded"].as(Hash)["4:embedded"].as(Hash)["6:varint"] = 0_i64 - when "new", "newest" - object["6:embedded"].as(Hash)["4:embedded"].as(Hash)["6:varint"] = 1_i64 - else # top - object["6:embedded"].as(Hash)["4:embedded"].as(Hash)["6:varint"] = 0_i64 - end - - continuation = object.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) } - - return continuation -end diff --git a/src/invidious/comments/youtube.cr b/src/invidious/comments/youtube.cr index c262876e..1ba1b534 100644 --- a/src/invidious/comments/youtube.cr +++ b/src/invidious/comments/youtube.cr @@ -4,9 +4,9 @@ module Invidious::Comments def fetch_youtube(id, cursor, format, locale, thin_mode, region, sort_by = "top") case cursor when nil, "" - ctoken = produce_comment_continuation(id, cursor: "", sort_by: sort_by) + ctoken = Comments.produce_continuation(id, cursor: "", sort_by: sort_by) when .starts_with? "ADSJ" - ctoken = produce_comment_continuation(id, cursor: cursor, sort_by: sort_by) + ctoken = Comments.produce_continuation(id, cursor: cursor, sort_by: sort_by) else ctoken = cursor end @@ -203,4 +203,48 @@ module Invidious::Comments return response end + + def produce_continuation(video_id, cursor = "", sort_by = "top") + object = { + "2:embedded" => { + "2:string" => video_id, + "25:varint" => 0_i64, + "28:varint" => 1_i64, + "36:embedded" => { + "5:varint" => -1_i64, + "8:varint" => 0_i64, + }, + "40:embedded" => { + "1:varint" => 4_i64, + "3:string" => "https://www.youtube.com", + "4:string" => "", + }, + }, + "3:varint" => 6_i64, + "6:embedded" => { + "1:string" => cursor, + "4:embedded" => { + "4:string" => video_id, + "6:varint" => 0_i64, + }, + "5:varint" => 20_i64, + }, + } + + case sort_by + when "top" + object["6:embedded"].as(Hash)["4:embedded"].as(Hash)["6:varint"] = 0_i64 + when "new", "newest" + object["6:embedded"].as(Hash)["4:embedded"].as(Hash)["6:varint"] = 1_i64 + else # top + object["6:embedded"].as(Hash)["4:embedded"].as(Hash)["6:varint"] = 0_i64 + end + + continuation = object.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) } + + return continuation + end end From f0c8477905e6aae5c3979a64dab964dc4b353fe0 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 6 May 2023 20:27:02 +0200 Subject: [PATCH 40/41] Comments: Move content-related functions to their own file --- src/invidious/{comments.cr => comments/content.cr} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/invidious/{comments.cr => comments/content.cr} (100%) diff --git a/src/invidious/comments.cr b/src/invidious/comments/content.cr similarity index 100% rename from src/invidious/comments.cr rename to src/invidious/comments/content.cr From 193c510c65cfc6c56f4409180b798c9eb8ef3efd Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 6 May 2023 20:53:39 +0200 Subject: [PATCH 41/41] Spec: Update require to point to new files --- spec/parsers_helper.cr | 2 +- spec/spec_helper.cr | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/parsers_helper.cr b/spec/parsers_helper.cr index bf05f9ec..6589acad 100644 --- a/spec/parsers_helper.cr +++ b/spec/parsers_helper.cr @@ -13,7 +13,7 @@ require "../src/invidious/helpers/utils" require "../src/invidious/videos" require "../src/invidious/videos/*" -require "../src/invidious/comments" +require "../src/invidious/comments/content" require "../src/invidious/helpers/serialized_yt_data" require "../src/invidious/yt_backend/extractors" diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index f8bfa718..b3060acf 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -7,7 +7,6 @@ require "../src/invidious/helpers/*" require "../src/invidious/channels/*" require "../src/invidious/videos/caption" require "../src/invidious/videos" -require "../src/invidious/comments" require "../src/invidious/playlists" require "../src/invidious/search/ctoken" require "../src/invidious/trending"